Dominando os Princípios SOLID: Do Código Frágil à Arquitetura Resiliente
Você já trabalhou em um projeto onde a simples adição de um campo em um formulário causou um bug no módulo de relatórios? Esse é o clássico sintoma do "código espaguete". Para evitar que sistemas de software se tornem ruins com o passar do tempo, Robert C. Martin (Uncle Bob) consolidou cinco princípios fundamentais de design de software: o SOLID.
1. S - Single Responsibility Principle (Princípio da Responsabilidade Única)
O Conceito: Uma classe deve ter uma, e apenas uma, razão para mudar. Isso significa que ela deve ter apenas uma responsabilidade dentro do sistema.
O Problema: Quando uma classe faz muitas coisas (a famosa God Class), ela cria um alto acoplamento. Se você precisar mudar a forma como o e-mail é formatado, corre o risco de quebrar a lógica de negócio que calcula o desconto.
Exemplo Ruim:
@Service
public class OrderProcessor {
public void process(Order order) {
// Responsabilidade 1: Regra de negócio
if (order.isValid()) {
// Responsabilidade 2: Acesso a dados
Database.save(order);
// Responsabilidade 3: Notificação
EmailSender.send("Pedido processado com sucesso!");
}
}
}
Exemplo Bom (Refatorado): Dividimos as responsabilidades. O OrderProcessor agora apenas orquestra o fluxo.
@Service
public class OrderProcessor {
private final OrderRepository repository;
private final NotificationService notificationService;
public OrderProcessor(OrderRepository repository, NotificationService notificationService) {
this.repository = repository;
this.notificationService = notificationService;
}
public void process(Order order) {
if (order.isValid()) {
repository.save(order);
notificationService.notifyCustomer(order);
}
}
}
2. O - Open/Closed Principle (Princípio Aberto/Fechado)
O Conceito: Entidades de software (classes, módulos, funções) devem estar abertas para extensão, mas fechadas para modificação. Você deve poder adicionar novos comportamentos sem alterar o código-fonte existente.
O Problema: Encher o código de
if/elseouswitch casetoda vez que um novo requisito chega. Se um novo meio de pagamento for adicionado, alterar a classePaymentServiceexistente pode gerar bugs colaterais.
Exemplo Ruim:
public class PaymentCalculator {
public double calculateFee(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
return amount * 0.05;
} else if (paymentType.equals("PIX")) {
return 0; // Pix não tem taxa
}
return amount * 0.01;
}
}
Exemplo Bom (Usando Polimorfismo e o padrão Strategy):
public interface PaymentStrategy {
double calculateFee(double amount);
}
public class CreditCardStrategy implements PaymentStrategy {
public double calculateFee(double amount) { return amount * 0.05; }
}
public class PixStrategy implements PaymentStrategy {
public double calculateFee(double amount) { return 0; }
}
// A classe abaixo está fechada para modificação!
// Se entrar Boleto amanhã, você só cria uma nova classe implementando a interface.
public class PaymentCalculator {
public double calculateFee(PaymentStrategy strategy, double amount) {
return strategy.calculateFee(amount);
}
}
3. L - Liskov Substitution Principle (Princípio da Substituição de Liskov)
O Conceito: Classes derivadas (filhas) devem poder ser substituídas por suas classes base (pais) sem que o comportamento do programa seja quebrado. Introduzido por Barbara Liskov, este princípio garante que a herança seja usada corretamente.
O Problema: Criar uma subclasse que altera radicalmente a regra da superclasse, forçando o cliente a usar
instanceofpara não quebrar o código, ou lançar exceções não previstas.
Exemplo Ruim: Imagine uma abstração de Conta Bancária.
public class BankAccount {
public void withdraw(double amount) { /* saca dinheiro */ }
}
// Conta Poupança não permite saque antes de um prazo.
public class SavingsAccount extends BankAccount {
@Override
public void withdraw(double amount) {
throw new UnsupportedOperationException("Não é possível sacar desta conta agora.");
}
}
Se o sistema iterar sobre uma lista de BankAccount e chamar withdraw(), a aplicação vai estourar quando encontrar a SavingsAccount.
Exemplo Bom: Reestruture a hierarquia para refletir a realidade do comportamento.
public abstract class Account {
public abstract double getBalance();
}
public interface Withdrawable {
void withdraw(double amount);
}
public class CheckingAccount extends Account implements Withdrawable {
public void withdraw(double amount) { /* saca dinheiro */ }
}
// SavingsAccount estende Account, mas NÃO implementa Withdrawable se não puder sacar.
public class SavingsAccount extends Account {
// Apenas visualiza saldo, sem herdar métodos que não consegue cumprir.
}
4. I - Interface Segregation Principle (Princípio da Segregação de Interfaces)
O Conceito: Uma classe não deve ser forçada a implementar interfaces e métodos que ela não vai usar. É melhor ter várias interfaces pequenas e específicas do que uma única interface "gorda" (fat interface).
O Problema: Quando você implementa uma interface e é forçado a deixar metade dos métodos vazios ou lançando exceções.
Exemplo Ruim:
public interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
// Uma impressora simples não tem scanner nem fax
public class SimplePrinter implements MultiFunctionDevice {
public void print() { /* Imprime */ }
public void scan() { throw new UnsupportedOperationException(); }
public void fax() { throw new UnsupportedOperationException(); }
}
Exemplo Bom: Segregue as interfaces por capacidades.
public interface Printer { void print(); }
public interface Scanner { void scan(); }
public interface Fax { void fax(); }
public class SimplePrinter implements Printer {
public void print() { /* Apenas imprime! */ }
}
public class AdvancedCopier implements Printer, Scanner {
public void print() { /* Imprime */ }
public void scan() { /* Escaneia */ }
}
5. D - Dependency Inversion Principle (Princípio da Inversão de Dependência)
O Conceito: Módulos de alto nível (regras de negócio) não devem depender de módulos de baixo nível (infraestrutura, banco de dados, APIs externas). Ambos devem depender de abstrações (interfaces). Além disso, abstrações não devem depender de detalhes, os detalhes é que devem depender de abstrações.
O Problema: Instanciar classes de infraestrutura diretamente dentro do domínio usando a palavra-chave
new. Isso impossibilita testes unitários isolados, já que você não consegue criar um Mock do banco de dados.
Exemplo Ruim:
public class UserService {
// Alto nível dependendo diretamente do baixo nível (implementação concreta MySQL)
private MySQLUserRepository repository = new MySQLUserRepository();
public void saveUser(User user) {
repository.save(user);
}
}
Exemplo Bom (Usando Spring Boot e Injeção de Dependências): A regra de negócio agora depende apenas do "contrato" (Interface). O Spring Boot se encarrega de injetar a implementação correta no momento da execução.
public interface UserRepository {
void save(User user);
}
@Repository
public class PostgresUserRepository implements UserRepository {
public void save(User user) { /* Salva no PostgreSQL */ }
}
@Service
public class UserService {
// Depende da abstração, não do detalhe
private final UserRepository repository;
// A inversão ocorre aqui (Injeção via construtor)
public UserService(UserRepository repository) {
this.repository = repository;
}
public void saveUser(User user) {
repository.save(user);
}
}
Conclusão
Aplicar o SOLID não significa escrever código perfeito na primeira tentativa. É sobre ter as ferramentas mentais para identificar o "mau cheiro" (code smell) no momento em que o código começa a ficar rígido. Entender profundamente esses 5 princípios transforma desenvolvedores comuns em engenheiros de software seniores capazes de projetar arquiteturas duradouras.
Recommended book
Want to dive deeper into this topic? Check out the book that inspired this post.
View book →