Pular para o conteúdo

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)

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.

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.

Para seguir o SRP, podemos dividir a classe Pedido em classes menores, cada uma com uma responsabilidade específica:

Pedido.java
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();
}
}
PedidoRepository.java
public class PedidoRepository {
private Database database;
public void salvar(Pedido pedido) {
database.save(pedido);
}
}
PedidoNotificador.java
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.

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.

Imagine uma classe FolhaDePagamento que calcula o salário de diferentes tipos de funcionários usando um switch ou if-else:

FolhaDePagamento.java
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.

Para seguir o OCP, podemos usar polimorfismo e criar uma hierarquia de classes para os funcionários:

Funcionario.java
public abstract class Funcionario {
public abstract double calcularSalario();
}
FuncionarioHorista.java
public class FuncionarioHorista extends Funcionario {
private int horasTrabalhadas;
private double valorHora;
@Override
public double calcularSalario() {
return horasTrabalhadas * valorHora;
}
}
FuncionarioMensalista.java
public class FuncionarioMensalista extends Funcionario {
private double salarioMensal;
@Override
public double calcularSalario() {
return salarioMensal;
}
}
FolhaDePagamento.java
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.

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.

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.

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.

Forma.java
public class Forma {
public int calcularArea() {
// Implementação genérica para formas
}
}
Retangulo.java
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;
}
}
Quadrado.java
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.

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.

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.

Para seguir o ISP, podemos criar interfaces mais específicas para cada tipo de veículo, evitando métodos desnecessários para certos clientes.

Veiculo.java
public interface VeiculoTerrestre {
void acelerar();
void frear();
}
VeiculoAereo.java
public interface VeiculoAereo {
void voar();
}
Carro.java
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
}
}
Aviao.java
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.

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.

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.

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.

EmailService.java
public interface EmailService {
void enviarEmail(String destinatario, String mensagem);
}
Gmail.java
public class Gmail implements EmailService {
@Override
public void enviarEmail(String destinatario, String mensagem) {
// Lógica para enviar email usando Gmail
}
}
ServicoEmail.java
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.

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.