Programação Java
Padrões de projeto estruturais e comportamentais em Java
Intermediário10 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Padrões estruturais definem como os objetos são compostos; padrões comportamentais, como eles colaboram e dividem responsabilidades. Este guia percorre os que você vai encontrar em qualquer base de código Java séria — Adapter, Decorator, Facade, Proxy e Composite; Strategy, Observer, Command, Template Method, Chain of Responsibility, State e Visitor — com implementações idiomáticas em Java 21, os lugares onde aparecem no JDK e no Spring, e como lambdas, tipos selados e pattern matching os simplificam.
O que você vai aprender
- Reconhecer Adapter, Decorator, Facade, Proxy e Composite no código de bibliotecas e frameworks
- Implementar Strategy, Command e Observer com interfaces funcionais quando fizer sentido
- Aplicar Template Method, Chain of Responsibility e State para eliminar condicionais
- Substituir Visitor por interfaces seladas e switch com pattern matching quando couber
- Explicar como os proxies do Spring e os streams de java.io materializam esses padrões
Depois que os objetos existem, sobram duas perguntas: como combiná-los em estruturas maiores e como dividir o comportamento entre eles para que cada mudança mexa em um só lugar. Os padrões estruturais e comportamentais do catálogo clássico respondem a essas perguntas e, ao contrário de alguns padrões criacionais, envelheceram bem: a maioria está à vista no JDK, no Spring e em qualquer framework web. Este guia reúne os doze que você mais provavelmente vai usar ou encontrar, dá a cada um uma implementação em Java 21 e mostra onde os recursos modernos da linguagem enxugam a forma clássica. Os padrões criacionais são tratados em outro guia.
Padrões estruturais: compondo objetos
Adapter
Intenção: converter a interface de uma classe na interface que o cliente espera, para que classes que de outra forma não conseguiriam trabalhar juntas consigam.
Você precisa de um adapter quando o código que chama e o código chamado são seus, mas a interface entre eles não — tipicamente ao integrar uma biblioteca de terceiros atrás da sua própria abstração:
public interface PaymentGateway {
PaymentResult charge(Money amount, CardToken card);
}
// Third-party SDK with a different vocabulary
public final class StripeGatewayAdapter implements PaymentGateway {
private final StripeClient stripe;
public StripeGatewayAdapter(StripeClient stripe) { this.stripe = stripe; }
@Override
public PaymentResult charge(Money amount, CardToken card) {
ChargeResponse response = stripe.charges().create(
new ChargeRequest(amount.minorUnits(), amount.currency().code(), card.value()));
return response.succeeded()
? new Approved(response.id())
: new Declined(response.failureCode());
}
}
O código da aplicação depende de PaymentGateway; o adapter traduz tipos, unidades e modelos de erro em um único lugar. Arrays.asList (de array para List), InputStreamReader (de stream de bytes para leitor de caracteres) e Collections.enumeration são adapters do JDK. É a forma mais limpa de impedir que o SDK de um fornecedor se espalhe pela base de código, e ainda torna esse fornecedor substituível.
Decorator
Intenção: anexar responsabilidades a um objeto dinamicamente, envolvendo-o em um objeto com a mesma interface.
Decorators se empilham: cada um implementa a interface, guarda outra instância dela e acrescenta algo antes ou depois de delegar. As classes de stream de java.io são o exemplo canônico — new BufferedInputStream(new GZIPInputStream(new FileInputStream(f))) — e o guia de I/O mostra essas classes em uso. Em código de serviço, a mesma forma adiciona comportamento transversal sem tocar na classe principal:
public interface ProductCatalogue {
Optional<Product> find(String sku);
}
public final class CachingCatalogue implements ProductCatalogue {
private final ProductCatalogue delegate;
private final Map<String, Optional<Product>> cache = new ConcurrentHashMap<>();
public CachingCatalogue(ProductCatalogue delegate) { this.delegate = delegate; }
@Override
public Optional<Product> find(String sku) {
return cache.computeIfAbsent(sku, delegate::find);
}
}
public final class TimingCatalogue implements ProductCatalogue {
private final ProductCatalogue delegate;
private final MeterRegistry metrics;
// ... constructor
@Override
public Optional<Product> find(String sku) {
long start = System.nanoTime();
try {
return delegate.find(sku);
} finally {
metrics.timer("catalogue.find").record(Duration.ofNanos(System.nanoTime() - start));
}
}
}
ProductCatalogue catalogue = new TimingCatalogue(new CachingCatalogue(new DatabaseCatalogue(ds)), metrics);
A ordem em que você envolve é a ordem do comportamento, e isso é ao mesmo tempo a força e a armadilha do padrão: com a medição de tempo por fora do cache, os cache hits também são medidos; com o cache por fora da medição, só os misses são medidos. Collections.unmodifiableList e synchronizedList são decorators, assim como todo advice do Spring AOP — e é por isso que o próximo padrão importa.
Proxy
Intenção: fornecer um substituto que controla o acesso a outro objeto.
Um proxy tem a mesma interface que o seu sujeito e, diferentemente de um decorator, existe para gerenciar esse sujeito: criá-lo sob demanda, protegê-lo com uma verificação de permissão, encaminhar as chamadas para uma implementação remota ou interceptá-las em nome de um framework. Java consegue gerar proxies em tempo de execução:
public interface AccountService { void close(String accountId); }
AccountService real = new DefaultAccountService();
AccountService guarded = (AccountService) Proxy.newProxyInstance(
AccountService.class.getClassLoader(),
new Class<?>[] { AccountService.class },
(proxy, method, args) -> {
if (!currentUser().hasRole("ADMIN")) {
throw new AccessDeniedException(method.getName());
}
return method.invoke(real, args); // InvocationHandler: intercept every call
});
java.lang.reflect.Proxy funciona com interfaces; CGLIB e ByteBuddy criam subclasses de classes concretas. É exatamente assim que o Spring implementa @Transactional, @Cacheable, @Async e as anotações de segurança: o bean que você injeta é um proxy em volta do seu objeto, e o comportamento da anotação roda na camada de interceptação. Isso também explica o clássico enigma do Spring — um método @Transactional chamado a partir de outro método da mesma classe não é transacional, porque a chamada interna nunca passa pelo proxy. Entidades JPA com lazy loading e stubs RMI também são proxies.
Facade
Intenção: oferecer uma interface única e simplificada para um subsistema.
Uma facade não acrescenta comportamento; ela esconde um conjunto de classes colaboradoras atrás de um único ponto de entrada, para que quem chama não precise conhecer a coreografia:
public final class CheckoutFacade {
private final InventoryService inventory;
private final PaymentGateway payments;
private final OrderRepository orders;
private final Notifier notifier;
// ... constructor
public OrderConfirmation checkout(Cart cart, CardToken card) {
ReservationToken reservation = inventory.reserve(cart.lines());
PaymentResult payment = payments.charge(cart.total(), card);
if (payment instanceof Declined d) {
inventory.release(reservation);
throw new PaymentDeclinedException(d.reason());
}
Order order = orders.save(Order.from(cart, payment));
notifier.orderPlaced(order);
return new OrderConfirmation(order.id());
}
}
Em uma arquitetura em camadas, um serviço de aplicação é uma facade sobre o domínio; javax.crypto.Cipher é uma facade sobre os providers; Files é, de certa forma, uma facade sobre os providers de sistema de arquivos. Costuma ser a primeira refatoração a aplicar em um controller que cresceu a ponto de orquestrar cinco serviços sozinho.
Composite
Intenção: compor objetos em estruturas de árvore e permitir que os clientes tratem objetos individuais e composições de maneira uniforme.
Sempre que algo pode conter coisas do mesmo tipo — diretórios e arquivos, containers e widgets de interface, grupos e itens de menu, nós de expressão —, o Composite dá a mesma interface à folha e ao container, de modo que as operações se tornam recursivas naturalmente:
public sealed interface Expense permits LineItem, ExpenseGroup {
long totalCents();
}
public record LineItem(String label, long cents) implements Expense {
@Override public long totalCents() { return cents; }
}
public record ExpenseGroup(String label, List<Expense> children) implements Expense {
@Override public long totalCents() {
return children.stream().mapToLong(Expense::totalCents).sum(); // recursion through the interface
}
}
Quem chama pede o total a qualquer Expense e nunca verifica se é um grupo. O Container extends Component do Swing, o DOM e o Path de uma varredura do sistema de arquivos são Composites. O custo do padrão é que operações que só fazem sentido para containers (add, remove) ou poluem a interface da folha ou exigem um cast; deixá-las apenas no tipo container, como acima, é a escolha mais segura.
Bridge e Flyweight completam o conjunto estrutural. Bridge separa uma abstração da sua implementação para que ambas possam variar — a Connection do JDBC sobre vários drivers — e, na prática, se resume a "programar para uma interface e injetar a implementação". Flyweight compartilha estado imutável entre muitos objetos para economizar memória; o cache de valores pequenos do Integer.valueOf e o interning de String são os exemplos do JDK, e records deduplicados por equals em um map são a forma moderna de construir um.
Padrões comportamentais: atribuindo responsabilidades
Strategy
Intenção: definir uma família de algoritmos intercambiáveis e deixar o cliente escolher um em tempo de execução.
O Strategy troca uma condicional sobre "qual algoritmo" por polimorfismo. Antes das lambdas, cada estratégia era uma classe; hoje, uma estratégia de método único é uma interface funcional, e uma interface nomeada fica onde o nome de domínio se justifica:
@FunctionalInterface
public interface ShippingRule {
Money cost(Shipment shipment);
static ShippingRule flat(Money amount) { return s -> amount; }
static ShippingRule perKilo(Money perKilo) { return s -> perKilo.times(s.weightKg()); }
static ShippingRule freeAbove(Money threshold, ShippingRule otherwise) {
return s -> s.orderValue().isGreaterThan(threshold) ? Money.ZERO : otherwise.cost(s);
}
}
Map<Region, ShippingRule> rules = Map.of(
Region.EU, ShippingRule.freeAbove(Money.eur(50), ShippingRule.flat(Money.eur(4))),
Region.US, ShippingRule.perKilo(Money.usd(3)));
Money cost = rules.get(shipment.region()).cost(shipment);
Comparator é o Strategy mais usado do JDK; ThreadFactory, RejectedExecutionHandler e toda Function que você passa para um stream também são. Um Map de chave para estratégia, como acima, substitui um switch que precisaria ser editado a cada novo caso.
Template Method
Intenção: fixar o esqueleto de um algoritmo em uma superclasse e deixar as subclasses sobrescreverem etapas específicas.
O guia de herança mostra a forma clássica com classe abstrata. A força dela é garantir a ordem das etapas; a fraqueza é depender de herança, então uma classe só pode seguir um template e o acoplamento é forte. A alternativa funcional passa as etapas variáveis como parâmetros, e é assim que funcionam JdbcTemplate, TransactionTemplate e RestTemplate no Spring: o template cuida da obtenção de recursos, da tradução de exceções e da limpeza, e um callback fornece o miolo. Quando você se pegar escrevendo o mesmo try/finally em volta de corpos diferentes, é um template esperando para ser extraído.
Observer
Intenção: quando um objeto muda de estado, notificar automaticamente todos os dependentes sem que o sujeito saiba quem eles são.
public final class OrderTracker {
private final List<Consumer<OrderEvent>> listeners = new CopyOnWriteArrayList<>();
public Runnable subscribe(Consumer<OrderEvent> listener) {
listeners.add(listener);
return () -> listeners.remove(listener); // unsubscribe handle
}
void publish(OrderEvent event) {
for (Consumer<OrderEvent> l : listeners) {
try {
l.accept(event);
} catch (RuntimeException e) {
log.warn("Listener failed for {}", event, e); // one bad listener must not stop the rest
}
}
}
}
CopyOnWriteArrayList torna seguro que um listener cancele a inscrição durante a notificação. Duas questões de design decidem se uma implementação de Observer vai dar dor de cabeça: de quem é a thread — a notificação síncrona bloqueia o publicador e herda a transação dele, enquanto a assíncrona precisa de um executor e perde as garantias de ordem; e como os listeners são removidos — um listener esquecido é um vazamento de memória, e é por isso que o método de inscrição devolve um handle para cancelar a inscrição. O ApplicationEventPublisher do Spring com @EventListener (e @TransactionalEventListener para disparar após o commit) é o padrão com toda essa gestão já resolvida; java.util.Observable está depreciado e o PropertyChangeSupport sobrevive principalmente em código desktop.
Command
Intenção: encapsular uma requisição como objeto, para que ela possa ser enfileirada, registrada em log, desfeita ou executada depois.
Um Runnable é um Command sem desfazer, e ExecutorService.submit é o invocador. O padrão mostra sua forma completa quando os comandos precisam de identidade e de reversão:
public interface EditorCommand {
void execute(Document doc);
void undo(Document doc);
}
public record InsertText(int offset, String text) implements EditorCommand {
@Override public void execute(Document doc) { doc.insert(offset, text); }
@Override public void undo(Document doc) { doc.delete(offset, text.length()); }
}
public final class CommandHistory {
private final Deque<EditorCommand> done = new ArrayDeque<>();
private final Document doc;
// ... constructor
public void run(EditorCommand cmd) { cmd.execute(doc); done.push(cmd); }
public void undo() { if (!done.isEmpty()) done.pop().undo(doc); }
}
Com records, comandos ficam baratos, fáceis de serializar à mão e de registrar em log. Além dos editores, o Command é a base de filas de jobs, command handlers de CQRS, transactional outboxes e logs de auditoria: tudo em que "o que foi pedido" precisa existir independentemente de "quem executa".
Chain of Responsibility
Intenção: passar uma requisição por uma cadeia de handlers até que um deles a trate, desacoplando o remetente do destinatário.
Filtros de servlet, a filter chain do Spring Security, interceptors de clientes HTTP e appenders de logging são todos cadeias. A forma moderna é uma lista de handlers com uma continuação next:
@FunctionalInterface
public interface Handler {
Response handle(Request request, Chain chain);
interface Chain { Response proceed(Request request); }
}
public final class Pipeline {
private final List<Handler> handlers;
private final Function<Request, Response> terminal;
// ... constructor
public Response run(Request request) { return proceed(0, request); }
private Response proceed(int index, Request request) {
if (index == handlers.size()) return terminal.apply(request);
return handlers.get(index).handle(request, r -> proceed(index + 1, r));
}
}
Pipeline pipeline = new Pipeline(List.of(
(req, chain) -> req.hasValidToken() ? chain.proceed(req) : Response.unauthorised(),
(req, chain) -> { log.info("{} {}", req.method(), req.path()); return chain.proceed(req); },
(req, chain) -> rateLimiter.allow(req.clientId()) ? chain.proceed(req) : Response.tooManyRequests()),
controller::dispatch);
Cada handler decide se para, se transforma a requisição ou se chama chain.proceed. Como um handler também pode agir depois que proceed retorna, a cadeia funciona ainda como uma pilha de advices "around". Mantenha os handlers sem estado e ordene-os explicitamente; uma cadeia montada por varredura do classpath em ordem indefinida é um pesadelo para depurar.
State
Intenção: permitir que um objeto mude de comportamento quando seu estado interno muda, delegando a um objeto de estado.
O State substitui um switch (status) repetido em todos os métodos por uma classe (ou constante de enum) por estado, que conhece as próprias transições:
public enum OrderStatus {
NEW {
@Override OrderStatus pay() { return PAID; }
@Override OrderStatus cancel() { return CANCELLED; }
},
PAID {
@Override OrderStatus ship() { return SHIPPED; }
@Override OrderStatus cancel() { return REFUNDED; }
},
SHIPPED, CANCELLED, REFUNDED;
OrderStatus pay() { throw new IllegalStateException("Cannot pay from " + this); }
OrderStatus ship() { throw new IllegalStateException("Cannot ship from " + this); }
OrderStatus cancel() { throw new IllegalStateException("Cannot cancel from " + this); }
}
Constantes de enum com corpo são a implementação de State mais leve em Java: cada transição fica em um só lugar, transições inválidas falham de forma ruidosa e o compilador enumera todos os estados. Quando os estados carregam dados ou precisam de serviços injetados, use uma interface selada com um record por estado e faça switch sobre ela. De qualquer forma, o benefício do padrão é que adicionar um estado significa adicionar uma classe, e não editar todos os métodos.
Visitor e seu substituto com tipos selados
Intenção: representar uma operação a ser executada sobre os elementos de uma estrutura de objetos sem alterar as classes desses elementos.
O Visitor clássico obtém double dispatch — comportamento escolhido tanto pelo tipo do elemento quanto pela operação — com um método accept(Visitor) em cada elemento e um método visitX por tipo de elemento no visitor. Adicionar uma operação é fácil (um novo visitor); adicionar um tipo de elemento é doloroso (todos os visitors mudam). É a forma certa para compiladores e modelos de documento com muitas operações sobre um conjunto estável de tipos de nó, e é assim que são construídas as APIs javax.lang.model e FileVisitor do JDK.
Java 21 oferece um caminho mais leve para as hierarquias que são suas: sele a interface e faça switch com record patterns.
public sealed interface Shape permits Circle, Rectangle, Group {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Group(List<Shape> children) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Group(List<Shape> kids) -> kids.stream().mapToDouble(Program::area).sum();
};
}
Cada operação é uma única função exaustiva; adicionar uma forma quebra todos os switches em tempo de compilação, que é exatamente a garantia que o Visitor dava por meio da interface. Mantenha o Visitor clássico quando a hierarquia for aberta, estiver em outro módulo ou quando uma instância de visitor precisar acumular estado ao longo de uma travessia de forma reutilizável.
Iterator, Mediator, Memento e Interpreter completam o catálogo comportamental. O Iterator está embutido na linguagem via Iterable e o loop for-each; implemente-o nos seus próprios agregados para que funcionem com streams (StreamSupport.stream(spliterator(), false)). O Mediator centraliza a comunicação entre componentes — um controller entre os campos de um formulário, um message broker entre serviços. O Memento captura um estado para restauração posterior; um record imutável servindo de snapshot geralmente basta. O Interpreter constrói um avaliador sobre uma gramática e é mais um encaixe natural para tipos selados.
Erros comuns
Design guiado por padrões. Escolher os padrões primeiro e depois procurar onde usá-los produz abstract factories de strategy visitors que ninguém consegue acompanhar. Comece pela duplicação ou pela mudança que você não consegue fazer de forma limpa e só então dê nome à solução.
Ordem dos decorators deixada implícita. Envolver na ordem errada (retry por fora do timeout, ou cache por fora da autorização) muda a semântica silenciosamente. Documente a ordem ou monte a pilha em um único factory method.
Autoinvocação passando por um proxy. Chamar um método anotado de dentro do mesmo bean do Spring contorna o proxy; mova o método para outro bean e só injete o bean nele mesmo como último recurso.
Observers que nunca cancelam a inscrição. O sujeito mantém uma referência forte, então o listener e tudo o que ele referencia vivem para sempre. Devolva handles de cancelamento e chame-os.
Bloquear ou lançar exceções dentro de listeners. Um listener lento trava o publicador; uma exceção pode impedir os listeners seguintes. Isole cada chamada e considere um executor.
Classes Strategy para uma linha só. Uma classe PlusOneStrategy implementando IntUnaryOperator é uma lambda fantasiada.
Visitor em uma hierarquia aberta que você não pode selar. Cada tipo novo quebra todos os visitors; use um default method ou um switch com default e aceite a perda da exaustividade.
Estado mutável em handlers compartilhados. Filtros e handlers de cadeia são usados de forma concorrente; um campo que muda a cada requisição é uma race condition.
Principais pontos
- Adapter muda uma interface, Decorator acrescenta comportamento atrás da mesma interface e Proxy controla o acesso — todos envolvendo o objeto, e todos visíveis em
java.io, nos utilitários de coleções e no Spring AOP. - Facade simplifica um subsistema atrás de um único ponto de entrada; Composite permite tratar árvores como objetos únicos por meio de uma interface comum.
- Strategy, Command e Observer hoje são, em grande parte, interfaces funcionais, lambdas e listas de
Consumer; mantenha tipos nomeados onde o significado de domínio ou a existência de vários métodos justificarem. - Template Method fixa a ordem das etapas; sua forma com callback (
JdbcTemplate) evita herança. - Chain of Responsibility e State eliminam condicionais: handlers com uma continuação
proceed, e constantes de enum ou records selados que são donos das próprias transições. - Interfaces seladas com
switche pattern matching substituem o Visitor nas hierarquias que você controla, mantendo a exaustividade sem o boilerplate.
Fontes e leituras complementares
Perguntas frequentes
- Qual a diferença entre Decorator, Proxy e Adapter?
- Os três envolvem um objeto, mas por motivos diferentes. Um Adapter muda a interface para que uma classe existente se encaixe onde outro tipo é esperado. Um Decorator mantém a mesma interface e acrescenta comportamento, e decorators podem ser empilhados. Um Proxy também mantém a interface, mas controla o acesso ao objeto real — criando-o sob demanda, verificando permissões, adicionando acesso remoto ou cache — e normalmente o cliente nem sabe que ele está ali.
- As lambdas tornam o padrão Strategy obsoleto?
- Elas tornam a hierarquia de classes opcional. Quando uma estratégia é uma única operação, uma interface funcional com lambdas ou method references é o padrão Strategy com menos código. Mantenha uma interface e classes nomeadas quando a estratégia tiver vários métodos, precisar de configuração ou estado, ou quando o nome carregar um significado de domínio que uma simples Function perderia.
- Devo usar o padrão Observer ou um barramento de eventos?
- O Observer direto — um sujeito que mantém uma lista de listeners — é o certo dentro de um mesmo componente, e é o que usam os toolkits de interface gráfica e o PropertyChangeSupport. Assim que vários componentes sem relação entre si reagem aos mesmos eventos, um mecanismo de eventos em processo como o ApplicationEventPublisher do Spring desacopla publicadores de assinantes e permite tornar os handlers assíncronos ou transacionais. Entre processos, use um message broker.
- O Visitor ainda é necessário agora que Java tem pattern matching?
- Para as suas próprias hierarquias fechadas, uma interface selada e um switch com record patterns oferecem o mesmo double dispatch, com verificação de exaustividade e muito menos boilerplate. O Visitor continua útil quando a hierarquia não é selada, quando ela está em uma biblioteca que você não pode alterar, ou quando você quer que o próprio visitor seja um componente reutilizável e com estado, passado de um lado para outro.