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 switch e 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.