paint-brush
Princípios SOLID em Java: um guia para iniciantespor@ps06756
14,605 leituras
14,605 leituras

Princípios SOLID em Java: um guia para iniciantes

por Pratik Singhal14m2024/03/22
Read on Terminal Reader

Muito longo; Para ler

Princípios SOLID são os princípios de programação orientada a objetos essenciais para desenvolver softwares escaláveis. Os princípios são: S: Princípio de Responsabilidade Única O: Princípio Aberto/Fechado L: Princípio da Substituição de Liskov I: Princípio de Segregação de Interface D: Princípio de Inversão de Dependência
featured image - Princípios SOLID em Java: um guia para iniciantes
Pratik Singhal HackerNoon profile picture
0-item

Os princípios de design SOLID são os princípios de design mais importantes que você precisa saber para escrever código limpo. Ter um domínio sólido sobre os princípios SOLID é uma habilidade indispensável para qualquer programador. Eles são a base sobre a qual outros padrões de projeto são desenvolvidos.


Neste artigo, abordaremos os princípios de design SOLID usando alguns exemplos da vida real e compreenderemos sua importância.


Juntamente com Polimorfismo, Abstração e Herança, os Princípios SOLID são realmente importantes para ser bom em programação orientada a objetivos.

Por que os princípios SOLID são importantes?

Os princípios SOLID são importantes por vários motivos:


  • Os princípios SOLID nos permitem escrever código limpo e de fácil manutenção: Quando iniciamos um novo projeto, inicialmente a qualidade do código é boa porque temos um conjunto limitado de recursos que implementamos. No entanto, à medida que incorporamos mais recursos, o código começa a ficar confuso.


  • Os Princípios SOLID baseiam-se nos fundamentos de Abstração, Polimorfismo e Herança e levam a padrões de design para casos de uso comuns. Compreender esses padrões de design ajuda a implementar casos de uso comuns em programação.


  • Os Princípios SOLID nos ajudam a escrever código limpo, o que melhora a testabilidade do código. Isso ocorre porque o código é modular e fracamente acoplado. Cada módulo pode ser desenvolvido e testado de forma independente.


Vamos agora explorar cada um dos princípios do SOLID detalhadamente com exemplos do mundo real.

1. Princípio da Responsabilidade Única

S nos princípios SOLID significa Princípio de Responsabilidade Única. O Princípio da Responsabilidade Única afirma que uma classe deve ter apenas um único motivo para mudar. Isso limita o número de locais que precisamos fazer alterações ao incorporar requisitos adicionais em nosso projeto.

Cada classe deve ter exatamente um motivo para mudar.

Por exemplo, digamos que estamos projetando um aplicativo bancário em Java onde temos uma classe SavingsAccount que permite operações básicas como débito, crédito e sendUpdates . O método sendUpdate pega um enum chamado NotificationMedium (como Email, SMS, etc.) e envia a atualização com a mídia apropriada. Escreveremos o código para isso conforme mostrado abaixo.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


Agora, se você observar a classe SavingsAccount acima, ela pode mudar por vários motivos:


  1. Se houver alguma alteração na lógica central da classe SavingsAccount (como debit , credit , etc.).


  2. Se o banco decidir introduzir um novo meio de notificação (digamos WhatsApp).


Isto é uma violação do Princípio da Responsabilidade Única nos Princípios SOLID. Para consertar, faremos uma classe separada que envia a notificação.


Vamos refatorar o código acima de acordo com os Princípios SOLID


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


Agora, como refatoramos o código, se houver alguma alteração no NotificationMedium ou no formato, alteraremos a classe Sender . Porém, se houver uma alteração na lógica central de SavingsAccount , haverá alterações na classe SavingsAccount .


Isso corrige a violação que observamos no primeiro exemplo.

2. Princípio Aberto/Fechado

O princípio Open Close afirma que devemos projetar as classes de tal forma que elas estejam abertas para extensão (no caso de adicionar recursos adicionais), mas fechadas para modificação. Estar fechado para modificação nos ajuda de duas maneiras:


  • Muitas vezes, a fonte da classe original pode nem estar disponível. Pode ser uma dependência consumida pelo seu projeto.


  • Manter a classe original inalterada reduz as chances de bugs. Visto que pode haver outras classes que dependem da classe que queremos modificar.


Para ver um exemplo do princípio Open Close, vamos dar uma olhada no exemplo de um carrinho de compras (como aquele implementado em sites de comércio eletrônico).


Vou criar uma classe chamada Cart que conterá uma lista de Item que você pode adicionar a ela. Dependendo do tipo de item e da tributação sobre ele queremos criar um método que calcule o valor total do carrinho dentro da classe Cart .


As aulas devem ser abertas para extensão e fechadas para modificação


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


No exemplo acima, o método calculateCartValue calcula o valor do carrinho iterando todos os itens dentro do carrinho e invocando a lógica com base no tipo de item.


Embora este código pareça correto, ele viola os Princípios SOLID.


Digamos que precisamos adicionar uma nova regra para um tipo diferente de item (por exemplo, Mercearia) ao calcular o valor do carrinho. Nesse caso, teríamos que modificar a classe original Cart e escrever outra condição else if dentro dela que verifica itens do tipo Grocery .


No entanto, com pouca refatoração, podemos fazer com que o código siga o princípio abrir/fechar. Vamos ver como.


Primeiro, tornaremos a classe Item abstrata e criaremos classes concretas para diferentes tipos de Item (s), conforme mostrado abaixo.


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


Dentro de cada classe de Item concreta, como GroceryItem , GiftItem e ElectronicItem , implemente o método getValue() que contém a lógica de negócios para tributação e cálculo de valor.


Agora, faremos com que a classe Cart dependa da classe abstrata Item e invocaremos o método getValue() para cada item conforme mostrado abaixo.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


Agora, neste código refatorado, mesmo que novos tipos de Item (s) sejam introduzidos, a classe Cart permanece inalterada. Devido ao polimorfismo, qualquer que seja o tipo real do Item dentro dos items ArrayList , o método getValue() dessa classe seria invocado.

3. Princípio da Substituição de Liskov

O princípio de substituição de Liskov afirma que em um determinado código, mesmo que substituamos o objeto de uma superclasse por um objeto da classe filha, o código não deve quebrar. Em outras palavras, quando uma subclasse herda uma superclasse e substitui seus métodos, ela deve manter consistência com o comportamento do método na superclasse.


Por exemplo, se fizermos as seguintes classes Vehicle e duas classes Car e Bicycle . Agora, digamos que criamos um método chamado startEngine() dentro da classe Vehicle, ele pode ser substituído na classe Car , mas não será suportado na classe Bicycle porque Bicycle não tem motor (veja o exemplo de código abaixo)


A subclasse deve manter consistência com o comportamento da superclasse ao substituir métodos.

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


Agora, digamos que exista algum código que espera um objeto do tipo veículo e depende do método startEngine() . Se, ao chamar esse trecho de código, em vez de passar um objeto do tipo Vehicle , passarmos um objeto Bicycle , isso causaria problemas no código. Já que o método da classe Bicycle (s) lançará uma exceção quando o método startEngine() for chamado. Isto seria uma violação dos Princípios SOLID (princípio de substituição de Liskov)


Para resolver esse problema, podemos criar duas classes MotorizedVehicle e NonMotorizedVehicle e fazer com que Car herde da classe MotorizedVehicle e que Bicycle herde de NonMotorizedVehicle


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. Princípio de segregação de interface

O “I” em Princípios SOLID significa Princípio de Segregação de Interface.


O princípio de segregação de interface afirma que, em vez de ter interfaces maiores que forçam a implementação de classes a implementar métodos não utilizados, devemos ter interfaces menores e ter classes implementadas. Dessa forma, as classes implementam apenas os métodos relevantes e permanecem limpas.

Divida suas interfaces em várias interfaces menores, em vez de uma interface grande.

Por exemplo, vejamos a estrutura interna de coleções em Java. Entre outras estruturas de dados, Java também fornece estruturas de dados LinkedList e ArrayList ,


A classe ArrayList implementa as seguintes interfaces: Serializable , Cloneable , Iterable , Collection , List e RandomAccess .

A classe LinkedList implementa Serializable , Cloneable , Iterable , Collection , Deque , List e Queue .


São muitas interfaces!


Em vez de ter tantas interfaces, os desenvolvedores Java poderiam ter combinado Serializable , Cloneable , Iterable , Collecton , List e RandomAccess em uma interface, digamos a interface IList . Agora, ambas as classes ArrayList e LinkedList poderiam ter implementado esta nova interface IList .


No entanto, como LinkedList não suporta acesso aleatório, ele poderia ter implementado os métodos na interface RandomAccess e poderia ter lançado UnsupportedOperationException quando alguém tentasse chamá-lo.


No entanto, isso seria uma violação do princípio de segregação de interface nos Princípios SOLID, pois “forçaria” a classe LinkedList a implementar os métodos dentro da interface RandomAccess , mesmo que não seja obrigatório.


Portanto, é melhor dividir a interface com base no comportamento comum e deixar que cada classe implemente muitas interfaces em vez de uma interface grande.

Princípio de Inversão de Dependência

O Princípio da Inversão de Dependência afirma que as classes de nível superior não devem depender diretamente das classes de nível inferior. Isso causa um acoplamento forte entre os dois níveis.


Em vez disso, as classes inferiores deveriam fornecer uma interface da qual as classes de nível superior deveriam depender.


Depende de interfaces em vez de classes


Por exemplo, vamos continuar com o exemplo do Cart que vimos acima e aprimorá-lo para adicionar algumas opções de pagamento. Vamos supor que temos dois tipos de opções de pagamento conosco, DebitCard e Paypal . Agora, na classe Cart , queremos adicionar um método ao placeOrder que calcularia o valor do carrinho e iniciaria o pagamento com base no pagamento fornecido. método.


Para fazer isso, poderíamos ter adicionado dependência no exemplo Cart acima, adicionando as duas opções de pagamento como campos dentro da classe Cart . No entanto, isso uniria fortemente a classe Cart com as classes DebitCard e Paypal .


Em vez disso, criaríamos uma interface Payment e faríamos com que as classes DebitCard e Paypal implementassem as interfaces Payment . Agora, a classe Cart dependerá da interface Payment , e não dos tipos de pagamento individuais. Isso mantém as classes fracamente acopladas.


Veja o código abaixo.


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


Se você estiver interessado em aprender mais sobre programação orientada a objetos, padrões de design GoF e entrevistas de design de baixo nível, verifique meu curso altamente avaliado Programação Orientada a Objetos + Padrões de Design Java