Princípios SOLID em Java
Aplique os princípios SOLID — SRP, OCP, LSP, ISP, DIP — ao design Java.
SOLID é um conjunto de cinco princípios de design orientado a objetos — popularizados por Robert C. Martin — que mantêm o código Java fácil de modificar, testar e estender à medida que cresce. Não são regras de sintaxe impostas pelo compilador; são diretrizes para onde definir fronteiras entre classes de modo que uma alteração não se propague por toda a base de código. O acrônimo representa Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation e Dependency Inversion.
Esses princípios se baseiam nos fundamentos da orientação a objetos: você verá interfaces, herança e polimorfismo sendo usados ao longo do texto. Se esses conceitos parecerem frágeis, revise-os primeiro — SOLID é, em grande parte, bom julgamento sobre onde aplicá-los.
Os cinco princípios em resumo
Cada letra aborda um tipo específico de problema de design. Mantenha esta tabela por perto enquanto lê o restante do capítulo:
| Letra | Princípio | Objetivo em uma linha |
|---|---|---|
| S | Single Responsibility | Uma classe deve ter apenas um motivo para mudar |
| O | Open/Closed | Aberta para extensão, fechada para modificação |
| L | Liskov Substitution | Subtipos devem ser usáveis onde quer que seu tipo base seja |
| I | Interface Segregation | Muitas interfaces pequenas valem mais do que uma grande |
| D | Dependency Inversion | Dependa de abstrações, não de classes concretas |
Os princípios se reforçam mutuamente. Em código bem estruturado, raramente se aplica apenas um — uma interface pequena (ISP) da qual código de alto nível depende (DIP) é exatamente o que permite adicionar uma nova implementação (OCP) sem tocar no chamador.
S — Princípio da Responsabilidade Única
Uma classe deve fazer uma coisa e ter um único motivo para mudar. Quando responsabilidades não relacionadas — regras de negócio e entrega de mensagens, por exemplo — compartilham uma classe, uma mudança em qualquer uma delas força a re-testar ambas. Separá-las isola as mudanças.
// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
void raise(String user, int errors) {
if (errors > 0) {
// ...build an email, open an SMTP connection, send...
}
}
}
// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) { this.notifier = notifier; }
void raise(String user, int errors) {
if (errors > 0) notifier.send(user, errors + " error(s) detected");
}
}O — Princípio Aberto/Fechado
Entidades de software devem estar abertas para extensão, mas fechadas para modificação. Você deve ser capaz de adicionar novo comportamento escrevendo código novo, não editando — e arriscando — código que já funciona. Em Java, o mecanismo usual é uma interface estável mais novas implementações.
interface Notifier { void send(String to, String message); }
class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier implements Notifier { /* ... */ } // new feature = new class
// AlertService never changes when a new channel appears.Adicionar notificações push futuramente significa escrever PushNotifier implements Notifier — AlertService permanece intacto, sem necessidade de revisão ou risco de regressão.
L — Princípio da Substituição de Liskov
Se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem quebrar o programa. Uma subclasse deve honrar o contrato de seu pai — mesmas expectativas, sem exceções surpreendentes, sem pré-condições mais restritivas.
abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle extends Shape { /* area() = PI * r * r */ }
// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}A violação clássica é Square extends Rectangle: se definir a largura também muta a altura, o código escrito para um Rectangle quebra quando recebe um Square. A solução é modelá-los como irmãos sob Shape, não como um par pai-filho. (Consulte classes abstratas para a base Shape usada aqui.)
I — Princípio da Segregação de Interface
Clientes não devem ser forçados a depender de métodos que não utilizam. Prefira várias interfaces pequenas e focadas a uma grande — caso contrário, um implementador é obrigado a criar stubs de métodos que não pode honrar.
// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }
// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }
class ConfigFile implements Readable { // no empty write() stub
public String read() { return "mode=prod"; }
}D — Princípio da Inversão de Dependência
Módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Na prática: codifique contra interfaces e injete a implementação concreta (a injeção por construtor é a forma mais simples). Isso é o que faz os outros princípios se pagarem — e o que torna uma classe testável, pois você pode passar um objeto falso.
// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.Um exemplo completo: todos os cinco em um programa
Este programa conecta os princípios — um único AlertService (SRP) se comunica com um Notifier injetado (DIP), alterna entre EmailNotifier e SmsNotifier sem mudanças (OCP), lê um ConfigFile apenas Readable (ISP) e soma áreas de subtipos de Shape de forma uniforme (LSP). Ele verifica seus próprios resultados para que você possa ver cada princípio em ação.
O que observar na execução:
email sent: [EMAIL -> alice: 3 error(s) detected]contém apenas uma entrada —bobtinha zero erros, entãoraisenão enviou nada.AlertServicetem a responsabilidade única de decidir quando alertar (SRP); nunca constrói o corpo da mensagem nem abre uma conexão.- A mesma classe
AlertServiceacionou tanto umEmailNotifierquanto umSmsNotifierporque a dependência foi passada pelo construtor (DIP). A lógica de alertas de alto nível depende apenas da interfaceNotifier, nunca de um remetente concreto. OCP check : ... unchanged = trueconfirma que ambos os objetos de alerta são da mesma classeAlertService: adicionar suporte a SMS significou escrever um novoSmsNotifier, sem nenhuma edição emAlertService— aberto para extensão, fechado para modificação.ISP check : is Writable? falsemostra queConfigFileimplementa apenasReadable. Como as interfaces são segregadas, a fonte somente de leitura nunca foi forçada a fornecer um stubwritesem sentido.LSP area : 9.142é a soma de um retângulo 2×3 (6,0) e um círculo de raio 1 (≈3,142).totalAreapercorreu referências deShapee chamouarea()sem verificar qual subtipo era — cada subtipo era substituível pelo seu base (LSP).