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 que permite operações básicas como débito, crédito e . O método pega um enum chamado (como Email, SMS, etc.) e envia a atualização com a mídia apropriada. Escreveremos o código para isso conforme mostrado abaixo. SavingsAccount sendUpdates sendUpdate NotificationMedium 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 acima, ela pode mudar por vários motivos: SavingsAccount Se houver alguma alteração na lógica central da classe (como , , etc.). SavingsAccount debit credit 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 ou no formato, alteraremos a classe . Porém, se houver uma alteração na lógica central de , haverá alterações na classe . NotificationMedium Sender SavingsAccount 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 que conterá uma lista de 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 Item 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 calcula o valor do carrinho iterando todos os itens dentro do carrinho e invocando a lógica com base no tipo de item. calculateCartValue 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 e escrever outra condição dentro dela que verifica itens do tipo . Cart else if 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 abstrata e criaremos classes concretas para diferentes tipos de (s), conforme mostrado abaixo. Item Item 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 , e , implemente o método que contém a lógica de negócios para tributação e cálculo de valor. GroceryItem GiftItem ElectronicItem getValue() Agora, faremos com que a classe dependa da classe abstrata e invocaremos o método para cada item conforme mostrado abaixo. Cart Item getValue() 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 (s) sejam introduzidos, a classe permanece inalterada. Devido ao polimorfismo, qualquer que seja o tipo real do dentro dos , o método dessa classe seria invocado. Item Cart Item items ArrayList getValue() 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 e duas classes e . Agora, digamos que criamos um método chamado dentro da classe Vehicle, ele pode ser substituído na classe , mas não será suportado na classe porque não tem motor (veja o exemplo de código abaixo) Vehicle Car Bicycle startEngine() Car Bicycle Bicycle 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 . Se, ao chamar esse trecho de código, em vez de passar um objeto do tipo , passarmos um objeto , isso causaria problemas no código. Já que o método da classe (s) lançará uma exceção quando o método for chamado. Isto seria uma violação dos Princípios SOLID (princípio de substituição de Liskov) startEngine() Vehicle Bicycle Bicycle startEngine() Para resolver esse problema, podemos criar duas classes e e fazer com que herde da classe e que herde de MotorizedVehicle NonMotorizedVehicle Car MotorizedVehicle Bicycle 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 e , LinkedList ArrayList A classe implementa as seguintes interfaces: , , , , e . ArrayList Serializable Cloneable Iterable Collection List RandomAccess A classe implementa , , , , , e . LinkedList Serializable Cloneable Iterable Collection Deque List Queue São muitas interfaces! Em vez de ter tantas interfaces, os desenvolvedores Java poderiam ter combinado , , , , e em uma interface, digamos a interface . Agora, ambas as classes e poderiam ter implementado esta nova interface . Serializable Cloneable Iterable Collecton List RandomAccess IList ArrayList LinkedList IList No entanto, como não suporta acesso aleatório, ele poderia ter implementado os métodos na interface e poderia ter lançado quando alguém tentasse chamá-lo. LinkedList RandomAccess UnsupportedOperationException 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 , mesmo que não seja obrigatório. RandomAccess 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 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, e . Agora, na classe , queremos adicionar um método ao que calcularia o valor do carrinho e iniciaria o pagamento com base no pagamento fornecido. método. Cart DebitCard Paypal Cart placeOrder Para fazer isso, poderíamos ter adicionado dependência no exemplo acima, adicionando as duas opções de pagamento como campos dentro da classe . No entanto, isso uniria fortemente a classe com as classes e . Cart Cart Cart DebitCard Paypal Em vez disso, criaríamos uma interface e faríamos com que as classes e implementassem as interfaces . Agora, a classe dependerá da interface , e não dos tipos de pagamento individuais. Isso mantém as classes fracamente acopladas. Payment DebitCard Paypal Payment Cart Payment 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