Padrões de Projeto (Design Patterns) em Java — o que são, por que usar, como usar e exemplos práticos
- O que são
- São soluções reutilizáveis e testadas para problemas recorrentes de design de software. Não são código pronto, mas modelos de como organizar classes, objetos e responsabilidades.
- Funcionam como um vocabulário comum entre desenvolvedores (ex.: “use um Strategy”, “isso pede um Observer”).
- São organizados em categorias:
- Criacionais: como criar objetos (Factory Method, Abstract Factory, Builder, Singleton, Prototype).
- Estruturais: como compor classes/objetos (Adapter, Facade, Decorator, Composite, Proxy, Bridge, Flyweight).
- Comportamentais: como objetos interagem (Strategy, Observer, Command, Template Method, State, Chain of Responsibility, Mediator, Iterator, Visitor, Memento, Interpreter).
- Por que usar
- Manutenção e evolução: código mais claro e extensível (aberto para extensão, fechado para modificação).
- Reuso de soluções: evita reinventar a roda e reduz risco de erros.
- Comunicação: facilita alinhamento entre time e revisão de código.
- Testabilidade: separa responsabilidades, permite mockar/alternar implementações.
- Como usar (passo a passo)
- Identifique o problema recorrente: “Preciso trocar um algoritmo sem mexer no cliente?”, “Preciso padronizar a criação de objetos complexos?”, “Preciso notificar vários interessados automaticamente?”.
- Escolha um padrão que se encaixe no contexto e nas restrições (tempo, acoplamento, performance).
- Modele as interfaces e classes seguindo a intenção do padrão (interfaces antes de classes concretas).
- Implemente incrementalmente e escreva testes para validar o comportamento e permitir troca de implementações.
- Revise: evite overengineering. Só introduza um padrão quando agrega clareza, flexibilidade ou reduz risco.
- Exemplos do dia a dia
- Strategy: escolher meio de transporte (carro, metrô, bicicleta) conforme trânsito/clima. Você mantém a “interface” de deslocamento e troca a estratégia.
- Observer: inscrição em notificações de promoções; a loja notifica todos os assinantes quando há oferta.
- Factory Method: máquina de café que prepara espresso, latte, cappuccino a partir de um “tipo” escolhido.
- Decorator: pedir um sorvete e adicionar coberturas (calda, castanhas) sem mudar o sorvete base.
- Facade: concierge do hotel que resolve reservas, táxi e ingressos por você com uma única interface.
- Adapter: adaptador de tomada que converte pinos para o padrão local.
- Command: garçom anota pedidos (comando) e a cozinha executa depois, podendo cancelar/repetir.
- State: catraca do metrô que se comporta diferente conforme o estado (travada, destravada).
- Chain of Responsibility: suporte ao cliente que escala o chamado do nível 1 para o 2 e assim por diante.
- Aplicação em sistemas Java (quando e onde usar)
- Em APIs e serviços:
- Strategy: para políticas de desconto, cálculo de frete, seleção de algoritmo (ex.: compressão, criptografia).
- Factory/Builder: para construir DTOs/entidades complexas, clientes HTTP configuráveis.
- Facade: para expor uma interface simples a um subsistema complexo (integrações).
- Decorator/Proxy: para adicionar cross-cutting (cache, retry, logging) sem alterar a lógica principal.
- Observer/Events: para propagar eventos de domínio (ex.: pedido criado → faturamento/notificação).
- Em frameworks:
- Spring usa Singleton como escopo padrão de beans e Proxy para AOP.
- HandlerAdapter/HandlerMapping em Spring MVC refletem Adapter/Strategy.
- RestTemplate/WebClient builders refletem Builder.
- Template Method em classes abstratas do Spring (ex.: AbstractView, AbstractController).
- Cinco padrões essenciais com mini-exemplos em Java
6.1) Strategy — troque algoritmos em tempo de execução
- Quando: você quer variar o “como” sem mudar o “o quê”.
- Analogia: escolher transporte (carro, metrô, bike) para ir ao trabalho.
interface FreteStrategy {
double calcular(double pesoKg, String cepDestino);
}
class FreteSedex implements FreteStrategy {
public double calcular(double pesoKg, String cep) { return 25 + 2.5 * pesoKg; }
}
class FreteEconomico implements FreteStrategy {
public double calcular(double pesoKg, String cep) { return 10 + 1.2 * pesoKg; }
}
class CalculadoraFrete {
private FreteStrategy strategy;
public CalculadoraFrete(FreteStrategy strategy) { this.strategy = strategy; }
public void setStrategy(FreteStrategy strategy) { this.strategy = strategy; }
public double calcular(double peso, String cep) { return strategy.calcular(peso, cep); }
}
// Uso
CalculadoraFrete calc = new CalculadoraFrete(new FreteSedex());
double valor = calc.calcular(3.0, "01000-000");
calc.setStrategy(new FreteEconomico());
double valor2 = calc.calcular(3.0, "01000-000");
6.2) Observer — notifique interessados automaticamente
- Quando: várias partes precisam reagir a uma mudança/evento.
- Analogia: newsletter/alerta de promoções.
interface PedidoListener {
void onCriado(Pedido pedido);
}
class Pedido {
private final List<PedidoListener> listeners = new ArrayList<>();
public void addListener(PedidoListener l) { listeners.add(l); }
public void criar() {
// ... lógica de criação
listeners.forEach(l -> l.onCriado(this));
}
}
// Observadores
class NotificadorEmail implements PedidoListener {
public void onCriado(Pedido pedido) { System.out.println("Email: pedido criado!"); }
}
class Faturamento implements PedidoListener {
public void onCriado(Pedido pedido) { System.out.println("Fatura gerada."); }
}
// Uso
Pedido p = new Pedido();
p.addListener(new NotificadorEmail());
p.addListener(new Faturamento());
p.criar();
6.3) Factory Method — delegue a criação ao subtipo
- Quando: subclasses decidem qual objeto concreto instanciar.
- Analogia: máquina de café que, a partir do tipo, sabe produzir a bebida.
abstract class LoggerCreator {
public Logger create() {
Logger logger = createLogger();
logger.configure(); // gancho comum
return logger;
}
protected abstract Logger createLogger();
}
class FileLoggerCreator extends LoggerCreator {
protected Logger createLogger() { return new FileLogger("app.log"); }
}
interface Logger { void configure(); void info(String msg); }
class FileLogger implements Logger {
public FileLogger(String path) {/*...*/}
public void configure() {/*...*/}
public void info(String msg) { /* escreve no arquivo */ }
}
// Uso
LoggerCreator creator = new FileLoggerCreator();
Logger logger = creator.create();
logger.info("start");
6.4) Decorator — adicione comportamentos sem herança pesada
- Quando: você quer “embelezar”/estender recursos dinamicamente.
- Analogia: sorvete com coberturas extras.
interface Relatorio {
String gerar();
}
class RelatorioBase implements Relatorio {
public String gerar() { return "dados"; }
}
abstract class RelatorioDecorator implements Relatorio {
protected final Relatorio wrappee;
protected RelatorioDecorator(Relatorio wrappee) { this.wrappee = wrappee; }
}
class RelatorioComTimestamp extends RelatorioDecorator {
public RelatorioComTimestamp(Relatorio w) { super(w); }
public String gerar() { return wrappee.gerar() + " | ts=" + System.currentTimeMillis(); }
}
class RelatorioComAssinatura extends RelatorioDecorator {
public RelatorioComAssinatura(Relatorio w) { super(w); }
public String gerar() { return wrappee.gerar() + " | assinatura=Equipe"; }
}
// Uso (composições diferentes)
Relatorio r = new RelatorioComAssinatura(new RelatorioComTimestamp(new RelatorioBase()));
System.out.println(r.gerar());
6.5) Facade — simplifique um subsistema complexo
- Quando: há muitas classes/etapas e você quer oferecer uma entrada única e simples.
- Analogia: concierge do hotel resolvendo várias tarefas com um único pedido.
class PagamentoService { void pagar() {/*...*/} }
class EstoqueService { void reservar() {/*...*/} }
class EntregaService { void despachar() {/*...*/} }
class PedidoFacade {
private final PagamentoService pagamento = new PagamentoService();
private final EstoqueService estoque = new EstoqueService();
private final EntregaService entrega = new EntregaService();
public void concluirPedido() {
pagamento.pagar();
estoque.reservar();
entrega.despachar();
}
}
// Uso
new PedidoFacade().concluirPedido();
- Diagrama Mermaid (exemplo do padrão Strategy)
classDiagram
class CalculadoraFrete {
-FreteStrategy strategy
+CalculadoraFrete(FreteStrategy)
+setStrategy(FreteStrategy) void
+calcular(peso: double, cep: String) double
}
class FreteStrategy {
<<interface>>
+calcular(peso: double, cep: String) double
}
class FreteSedex {
+calcular(peso: double, cep: String) double
}
class FreteEconomico {
+calcular(peso: double, cep: String) double
}
FreteStrategy <|.. FreteSedex
FreteStrategy <|.. FreteEconomico
CalculadoraFrete --> FreteStrategy
- Boas práticas e armadilhas
- Evite overengineering: não introduza padrões sem necessidade real.
- Prefira composição a herança quando possível (Decorator, Strategy).
- Nomeie bem as interfaces/classes para deixar a intenção óbvia.
- Cubra com testes de unidade e contratuais (interface) para alternar implementações com segurança.
- Documente o “por que” do padrão no local do código (JavaDoc) para facilitar manutenção.
- Em Java, use recursos do ecossistema: Spring (AOP/Proxy, Events/Observer), Lombok (Builder), registros (Record) quando for apenas dados.
- Checklist rápido para escolher um padrão
- Preciso variar algoritmo? → Strategy/Template Method.
- Preciso notificação reativa? → Observer/Eventos.
- Criação flexível/complexa? → Factory Method/Abstract Factory/Builder.
- Interface simples para subsistemas complexos? → Facade.
- Adicionar comportamentos sem herança? → Decorator.
- Controlar instância única/global? → Singleton (com cuidado).
- Controlar acesso/remoto/lazy? → Proxy.
- Encadear responsabilidades? → Chain of Responsibility.
- Estados alteram comportamento? → State.