SOLID
Os princípios SOLID são um conjunto de diretrizes para o design de software orientado a objetos, que ajudam a criar sistemas mais flexíveis, modulares e fáceis de manter.
O SOLID foi proposto por Robert C. Martin, também conhecido como “Uncle Bob”, no artigo “Design Principles and Design Patterns”, de 2000. É composto por cinco princípios fundamentais que orientam a estruturação e organização do código.
Eles são:
- S: Single Responsibility Principle (Princípio da Responsabilidade Única)
- O: Open/Closed Principle (Princípio Aberto/Fechado)
- L: Liskov Substitution Principle (Princípio da Substituição de Liskov)
- I: Interface Segregation Principle (Princípio da Segregação de Interface)
- D: Dependency Inversion Principle (Princípio da Inversão de Dependência)
Single Responsibility Principle (SRP)
Seção intitulada “Single Responsibility Principle (SRP)”O SRP afirma que uma classe deve ter apenas uma razão para mudar, ou seja, deve ter apenas uma responsabilidade. Isso torna o código mais coeso e fácil de manter, pois cada classe tem um propósito claro e definido.
Violando o princípio
Seção intitulada “Violando o princípio”Imagine uma classe Pedido abaixo que é responsável por gerenciar os itens do pedido, calcular o total, salvar o pedido no banco de dados e enviar notificações por email:
public class Pedido { private Database database; private EmailService emailService; private List<Item> itens;
public void adicionarItem(Item item) { itens.add(item); }
public double calcularTotal() { return itens.stream().mapToDouble(Item::getPreco).sum(); }
public void salvar() { database.save(this); }
public void enviarNotificacao() { emailService.send("Pedido criado", "Seu pedido foi criado com sucesso."); }}Essa classe viola o SRP porque tem múltiplas responsabilidades: gerenciar itens, calcular total, salvar no banco de dados e enviar notificações. Se precisarmos alterar a forma como os pedidos são salvos ou como as notificações são enviadas, teremos que modificar a classe Pedido, o que pode introduzir bugs em outras funcionalidades.
Dividindo responsabilidades
Seção intitulada “Dividindo responsabilidades”Para seguir o SRP, podemos dividir a classe Pedido em classes menores, cada uma com uma responsabilidade específica:
public class Pedido { private List<Item> itens;
public void adicionarItem(Item item) { itens.add(item); }
public double calcularTotal() { return itens.stream().mapToDouble(Item::getPreco).sum(); }}public class PedidoRepository { private Database database;
public void salvar(Pedido pedido) { database.save(pedido); }}public class PedidoNotificador { private EmailService emailService;
public void enviarNotificacao(Pedido pedido) { emailService.send("Pedido criado", "Seu pedido foi criado com sucesso."); }}Dessa forma, cada classe tem uma única responsabilidade, tornando o código mais modular, coeso e fácil de manter.
Open/Closed Principle (OCP)
Seção intitulada “Open/Closed Principle (OCP)”O OCP afirma que as entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação. Isso significa que devemos ser capazes de adicionar novas funcionalidades sem alterar o código existente, evitando assim introduzir bugs em funcionalidades já testadas.
Violando o princípio
Seção intitulada “Violando o princípio”Imagine uma classe FolhaDePagamento que calcula o salário de diferentes tipos de funcionários usando um switch ou if-else:
public class FolhaDePagamento { public double calcularSalario(Funcionario funcionario) { if (funcionario instanceof FuncionarioHorista) { return ((FuncionarioHorista) funcionario).getHorasTrabalhadas() * ((FuncionarioHorista) funcionario).getValorHora(); } else if (funcionario instanceof FuncionarioMensalista) { return ((FuncionarioMensalista) funcionario).getSalarioMensal(); } // Outros tipos de funcionários... return 0; }}Essa implementação viola o OCP porque, para adicionar um novo tipo de funcionário, precisamos modificar a classe FolhaDePagamento, o que pode introduzir bugs em outras partes do código.
Seguindo o princípio
Seção intitulada “Seguindo o princípio”Para seguir o OCP, podemos usar polimorfismo e criar uma hierarquia de classes para os funcionários:
public abstract class Funcionario { public abstract double calcularSalario();}public class FuncionarioHorista extends Funcionario { private int horasTrabalhadas; private double valorHora;
@Override public double calcularSalario() { return horasTrabalhadas * valorHora; }}public class FuncionarioMensalista extends Funcionario { private double salarioMensal;
@Override public double calcularSalario() { return salarioMensal; }}public class FolhaDePagamento { public double calcularSalario(Funcionario funcionario) { return funcionario.calcularSalario(); }}Assim, a classe FolhaDePagamento depende da abstração Funcionario, e não de implementações concretas. Para adicionar um novo tipo de funcionário, basta criar uma nova classe que estenda Funcionario e implementar o método calcularSalario(), sem precisar modificar a classe FolhaDePagamento.
Liskov Substitution Principle (LSP)
Seção intitulada “Liskov Substitution Principle (LSP)”O LSP afirma que objetos de uma classe derivada devem ser substituíveis por objetos da classe base sem alterar as propriedades desejáveis do programa. Isso significa que as subclasses devem ser capazes de substituir suas superclasses sem causar erros ou comportamentos inesperados.
Violando o princípio
Seção intitulada “Violando o princípio”Uma violação do LSP ocorre quando uma subclasse não pode ser usada no lugar de sua classe base sem causar problemas. Por exemplo, considere uma classe Retangulo e uma subclasse Quadrado. Embora um quadrado seja um tipo de retângulo na geometria, essa relação não funciona bem no design de software devido às diferenças no comportamento esperado.
public class Retangulo { protected int largura; protected int altura;
public void setLargura(int largura) { this.largura = largura; }
public void setAltura(int altura) { this.altura = altura; }
public int calcularArea() { return largura * altura; }}
public class Quadrado extends Retangulo { @Override public void setLargura(int largura) { this.largura = largura; this.altura = largura; // Força a altura a ser igual à largura }
@Override public void setAltura(int altura) { this.altura = altura; this.largura = altura; // Força a largura a ser igual à altura }}Neste exemplo, a classe Quadrado viola o LSP porque altera o comportamento esperado da classe base Retangulo. Se um cliente espera que a largura e a altura possam ser definidas independentemente, o uso de Quadrado no lugar de Retangulo causará comportamentos inesperados.
Seguindo o princípio
Seção intitulada “Seguindo o princípio”Para seguir o LSP, podemos evitar a herança entre Retangulo e Quadrado e, em vez disso, criar uma hierarquia de classes que não force essa relação de substituição inadequada.
public class Forma { public int calcularArea() { // Implementação genérica para formas }}public class Retangulo extends Forma { private int largura; private int altura;
public void setLargura(int largura) { this.largura = largura; }
public void setAltura(int altura) { this.altura = altura; }
@Override public int calcularArea() { return largura * altura; }}public class Quadrado extends Forma { private int lado;
public void setLado(int lado) { this.lado = lado; }
@Override public int calcularArea() { return lado * lado; }}Dessa forma, Quadrado e Retangulo são classes separadas que não violam o LSP, pois cada uma tem seu próprio comportamento específico e não interfere nas expectativas do cliente em relação à outra.
Interface Segregation Principle (ISP)
Seção intitulada “Interface Segregation Principle (ISP)”O ISP afirma que os clientes não devem ser forçados a depender de interfaces que não utilizam. Isso significa que devemos criar interfaces específicas para cada cliente, em vez de uma interface geral que contenha métodos que nem todos os clientes precisam.
Violando o princípio
Seção intitulada “Violando o princípio”Considere uma interface Veiculo que contém métodos genéricos para veículos, como acelerar(), frear() e voar(). Se uma classe Carro for forçada a implementar o método voar(), isso violará o ISP, pois um carro não tem a capacidade de voar.
public interface Veiculo { void acelerar(); void frear(); void voar(); // Método desnecessário para veículos terrestres}
public class Carro implements Veiculo { @Override public void acelerar() { // Lógica para acelerar um carro }
@Override public void frear() { // Lógica para frear um carro }
@Override public void voar() { throw new UnsupportedOperationException("Carros não podem voar"); }}Neste exemplo, a classe Carro é forçada a implementar o método voar(), mesmo que esse método não seja relevante para ela. Isso cria um design inadequado e viola o ISP, pois os clientes da interface Veiculo podem esperar que todos os métodos sejam utilizáveis, o que não é o caso aqui.
Seguindo o princípio
Seção intitulada “Seguindo o princípio”Para seguir o ISP, podemos criar interfaces mais específicas para cada tipo de veículo, evitando métodos desnecessários para certos clientes.
public interface VeiculoTerrestre { void acelerar(); void frear();}public interface VeiculoAereo { void voar();}public class Carro implements VeiculoTerrestre { @Override public void acelerar() { // Lógica para acelerar um carro }
@Override public void frear() { // Lógica para frear um carro }}public class Aviao implements VeiculoAereo { @Override public void voar() { // Lógica para voar um avião }}Dessa forma, a classe Carro implementa apenas a interface VeiculoTerrestre, que contém métodos relevantes para ela, enquanto a classe Aviao implementa a interface VeiculoAereo, que contém métodos relevantes para aviões. Isso segue o ISP e cria um design mais limpo e adequado para cada tipo de veículo.
Dependency Inversion Principle (DIP)
Seção intitulada “Dependency Inversion Principle (DIP)”O DIP afirma que os módulos de alto nível não devem depender de módulos de baixo nível, ambos devem depender de abstrações. Além disso, as abstrações não devem depender de detalhes, os detalhes devem depender de abstrações. Isso promove a desacoplagem entre os módulos e facilita a manutenção e evolução do código.
Violando o princípio
Seção intitulada “Violando o princípio”Considere uma classe ServicoEmail que depende diretamente de uma implementação concreta, como Gmail. Isso cria um acoplamento forte entre as duas classes, dificultando a substituição de Gmail por outro provedor de email no futuro.
public class Gmail { public void enviarEmail(String destinatario, String mensagem) { // Lógica para enviar email usando Gmail }}
public class ServicoEmail { private Gmail gmail;
public ServicoEmail() { this.gmail = new Gmail(); // Dependência direta em uma implementação concreta }
public void enviar(String destinatario, String mensagem) { gmail.enviarEmail(destinatario, mensagem); }}Neste exemplo, a classe ServicoEmail está diretamente acoplada à classe Gmail. Se quisermos usar outro provedor de email, como Outlook, precisaríamos modificar o código da classe ServicoEmail, violando o DIP. Isso dificulta a manutenção e a extensibilidade do sistema.
Seguindo o princípio
Seção intitulada “Seguindo o princípio”Para seguir o DIP, podemos introduzir uma abstração, como uma interface EmailService, e fazer com que ServicoEmail dependa dessa abstração em vez de uma implementação concreta.
public interface EmailService { void enviarEmail(String destinatario, String mensagem);}public class Gmail implements EmailService { @Override public void enviarEmail(String destinatario, String mensagem) { // Lógica para enviar email usando Gmail }}public class ServicoEmail { private EmailService emailService;
public ServicoEmail(EmailService emailService) { this.emailService = emailService; // Dependência invertida para uma abstração }
public void enviar(String destinatario, String mensagem) { emailService.enviarEmail(destinatario, mensagem); }}Nesse caso, a classe ServicoEmail depende da abstração EmailService, e podemos facilmente substituir Gmail por outra implementação, como Outlook, sem modificar o código de ServicoEmail, seguindo assim o DIP.
Conclusão
Seção intitulada “Conclusão”Seguir os princípios SOLID é fundamental para criar software de alta qualidade, que seja fácil de entender, manter e evoluir. Esses princípios ajudam a evitar o acoplamento excessivo, promovem a reutilização de código e facilitam a adaptação do software a mudanças futuras. Ao aplicar os princípios SOLID, os desenvolvedores podem criar sistemas mais robustos e flexíveis, que atendam melhor às necessidades dos usuários e sejam mais fáceis de manter ao longo do tempo.