Skip to content

Padrões Arquiteturais

Arquiteturas de código

Os padrões descritos a partir da próxima seção deste catálogo resolvem problemas na escala de classes e objetos individuais. Antes disso, vale entender um nível acima: como organizar a arquitetura de um projeto inteiro em camadas ou módulos, buscando a mesma alta coesão e baixo acoplamento já discutidos em Boas Práticas, só que na escala do sistema como um todo.

Definição: Baixa coesão e alto acoplamento (cenário ruim)

"Quando tudo depende de tudo, nada pode ser alterado sem afetar o restante do sistema." Um sintoma comum: um único módulo concentrando responsabilidades de contextos completamente diferentes (cálculo de valores, envio de e-mail, controle de estoque, cadastro de endereço) e dependendo diretamente de outras partes do sistema, sem nenhuma abstração entre elas — qualquer mudança num desses pontos se propaga para os demais.

Definição: Alta coesão e baixo acoplamento (cenário ideal)

"Quando cada parte faz bem o seu trabalho e depende pouco das outras, o sistema evolui com segurança e clareza." Cada módulo tem uma única responsabilidade clara, e a comunicação entre módulos diferentes acontece através de interfaces bem definidas (ou eventos), nunca por acesso direto aos detalhes internos uns dos outros — o que permite alterar a implementação de um módulo sem impactar os demais.

MVC (Model-View-Controller)

Definição: MVC (Model-View-Controller)

Criado na década de 1970, é um padrão de separação de responsabilidades que organiza a camada de apresentação de uma aplicação em três componentes: o Model, que representa os dados e as regras de negócio; a View, que exibe informações para o usuário; e o Controller, que lida com a entrada do usuário e orquestra a comunicação entre Model e View. Popularizado por frameworks como Spring MVC, ASP.NET MVC e AngularJS.

flowchart TD
    View["View (Formulário)"] -->|"Submete dados<br/>POST /task"| Controller
    Controller["Controller (Endpoint REST)"] -->|"Valida os valores<br/>de entrada"| Model
    Model["Model (Serviço)"] -->|"Executa regras de<br/>negócio e persistência"| DB[(Banco de dados)]
    Model -->|"Retorno do resultado"| View

Definição: MVC não é uma arquitetura completa

O MVC nasceu para organizar só a camada de apresentação — ele não diz nada sobre como estruturar as regras de negócio ou o acesso a dados por trás do Model. Na prática, é comum o Controller acumular regras de negócio com o tempo, ferindo o SRP do SOLID e dificultando a testabilidade — e a separação entre apresentação e negócio nem sempre é respeitada, misturando as duas responsabilidades. Os padrões a seguir (Camadas, Hexagonal, Clean Architecture) surgiram, em parte, para resolver exatamente essas limitações.

Arquitetura em Camadas (Layered Architecture)

Definição: Arquitetura em Camadas (Layered Architecture)

Agrupa conceitualmente as responsabilidades do código em pacotes com dependência sequencial entre eles — como um prédio, onde cada andar só acessa o imediatamente abaixo. Normalmente segue três grandes blocos: uma camada de apresentação (entrypoints — REST controllers, consumidores de fila, CLI), uma camada de domínio/serviços (regras de negócio) e uma camada de persistência de dados (DAO — Data Access Object).

flowchart LR
    Controllers --> Domain --> DAO
classDiagram
    class TasksRestController
    class TaskService {
        <<interface>>
        +execute(toCreate: Task)
    }
    class CreateTaskService {
        +execute(toCreate: Task)
    }
    class JpaTaskRepository
    TasksRestController ..> TaskService : Uses
    TaskService <|.. CreateTaskService
    CreateTaskService ..> JpaTaskRepository : Uses
public record Task(String name, LocalDate startDate) {
    public boolean isValidStartDate() {
        return startDate.isAfter(LocalDate.now());
    }
}

@Service
public class CreateTaskService {
    private final JpaTaskRepository jpaTaskRepository; // domínio acoplado ao DAO

    public CreatedTaskResponseModel execute(Task toCreate) {
        if (jpaTaskRepository.existsByName(toCreate.name())) {
            throw new BusinessException("Task already exists.");
        }
        if (!toCreate.isValidStartDate()) {
            throw new BusinessException("Task start date should be greater than actual date.");
        }
        jpaTaskRepository.save(toCreate);
        return new CreatedTaskResponseModel(toCreate.name(), toCreate.startDate());
    }
}

Definição: A Arquitetura em Camadas não prioriza o isolamento do domínio

A camada de domínio (CreateTaskService, acima) depende diretamente de JpaTaskRepository (a camada de detalhes), ferindo o DIP do SOLID — ao trocar o mecanismo de persistência, é necessário alterar a implementação do domínio, mesmo que nenhuma regra de negócio tenha mudado. Como consequência direta, não é possível testar unitariamente CreateTaskService sem depender da implementação real (ou de um mock) da camada de detalhes — indo contra a mesma direção de dependência que o DIP recomenda (instável → estável, nunca o contrário).

Definição: Quando a Arquitetura em Camadas ainda faz sentido

Funciona bem em aplicações onde a equipe tem baixo domínio de tecnologias e padrões arquiteturais mais sofisticados, ou que precisam de uma entrega rápida (quick win) para um escopo pequeno, sem previsão de mudanças futuras de peso. Para aplicações que vão crescer e evoluir por muito tempo, Hexagonal Architecture e Clean Architecture (a seguir), em conjunto com DDD, tendem a compensar melhor o investimento inicial extra.

Arquitetura Hexagonal (Ports and Adapters)

Definição: Arquitetura Hexagonal (Ports and Adapters)

Proposta por Alistair Cockburn em 2005, tem como princípio central garantir que o domínio lide exclusivamente com regras de negócio, enquanto toda interação com o mundo externo ocorre por meio de portas de entrada (input ports) e portas de saída (output ports/adapters). O fluxo de dependência sempre aponta para dentro do domínio — representado metaforicamente como um hexágono —, em conformidade direta com o DIP do SOLID.

flowchart LR
    subgraph Application
        UI["User Interaction"]
    end
    subgraph Domain
        IP["Input Port"] --> Core["Core Business<br/>Implementation"] --> OP["Output Port"]
    end
    subgraph Infrastructure
        Adapter
    end
    UI -->|Use| IP
    OP -->|Implement| Adapter
  • Application é a camada responsável pelos mecanismos de interação com o cliente — um REST controller, um consumidor de Kafka — sem que esses detalhes interessem ao domínio.
  • Domain é a camada responsável pelas regras de negócio e pela definição das portas de entrada e saída, agnóstica aos detalhes exigidos por Application e Infrastructure: o foco é totalmente o que não fazer e não como será feito.
  • Infrastructure é a camada responsável pelas implementações concretas — banco de dados, integrações externas, obtenção de dados via arquivo.
classDiagram
    class RestController
    class ITaskService {
        <<interface>>
        +execute()
    }
    class TaskService
    class TaskDsGateway {
        <<interface>>
    }
    class JpaTaskDataProviderAdapter
    RestController ..> ITaskService : Use
    ITaskService <|.. TaskService : Implement
    TaskService ..> TaskDsGateway : Use
    TaskDsGateway <|.. JpaTaskDataProviderAdapter : Implement

O REST controller (que poderia ser substituído por qualquer outro ponto de entrada, como uma classe de testes) injeta a interface ITaskService, associada em tempo de execução a uma instância concreta de TaskService pelo mecanismo de injeção de dependências. Para persistir a tarefa, TaskService usa TaskDsGateway — uma porta de saída cuja implementação (JpaTaskDataProviderAdapter) é só um detalhe substituível, sem impacto no domínio.

Definição: Testabilidade quase total do domínio

O isolamento promovido pela Arquitetura Hexagonal torna simples testar o domínio de negócio inteiro sem tocar nenhum detalhe de implementação — na camada Application, uma classe de teste chama diretamente a porta de entrada; na camada Infrastructure, um mock substitui a porta de saída real. O domínio apresenta forte coesão e nenhum acoplamento com as demais camadas.

Arquitetura Limpa (Clean Architecture)

Definição: Clean Architecture

Publicada em 2012 por Robert C. Martin, é vista por muitos como uma evolução da Arquitetura Hexagonal, especificando mais camadas para o domínio de negócio: Entities (regras de negócio mais genéricas, reaproveitáveis por outras aplicações do mesmo contexto) e Use Cases (ações específicas do sistema atual), com o uso de Gateways que, por sua vez, especificam o que deve ser feito, sem se preocupar com como deve ser feito.

Definição: Entities x Use Cases

Entities guardam as regras de negócio enterprise — as mais puras da aplicação, potencialmente reaproveitáveis em outros sistemas do mesmo domínio — e não devem depender de nenhuma outra camada. Use Cases guardam regras específicas da aplicação atual, orquestrando uma ou mais Entities num processo conhecido como Dança das Entidades — sem, ainda assim, se acoplar a detalhes de infraestrutura, que ficam por conta dos Gateways.

// Entity: regra de negócio pura, sem dependência de nenhuma outra camada
public record Task(String name, LocalDate startDate) {
    public boolean isValidStartDate() {
        return startDate.isAfter(LocalDate.now());
    }
}
// Use Case: orquestra a Entity, mas depende só de abstrações (Gateway e Presenter)
public interface TaskCreateInputBoundary {
    CreatedTaskResponseModel execute(Task toCreate);
}

@Service
public class TaskRegisterInteractor implements TaskCreateInputBoundary {
    private final TaskDsGateway taskDsGateway;
    private final TaskPresenter taskPresenter;

    public CreatedTaskResponseModel execute(Task toCreate) {
        if (taskDsGateway.existsByName(toCreate.name())) {
            taskPresenter.prepareFailView("Task already exists.");
        }
        if (!toCreate.isValidStartDate()) {
            taskPresenter.prepareFailView("Task start date should be greater than actual date.");
        }
        taskDsGateway.save(toCreate);
        return taskPresenter.prepareSuccessView(/* ... */);
    }
}

Definição: Input Boundary, Interactor, Gateway e Presenter

A Clean Architecture nomeia os elementos das portas de entrada e saída já vistos na Arquitetura Hexagonal com termos próprios: o Input Boundary é o contrato (a porta de entrada) que o REST controller (ou qualquer outro entrypoint) invoca; o Interactor é a implementação concreta do caso de uso; o Gateway é a porta de saída para persistência (o equivalente ao TaskDsGateway da Hexagonal); e o Presenter é uma porta de saída adicional, introduzida por essa arquitetura, para formatar o retorno do caso de uso da forma que cada entrypoint específico espera — uma API REST devolve o resultado em JSON; um consumidor de fila talvez precise só publicar noutro tópico.

classDiagram
    class Task {
        -name: String
        -startDate: LocalDate
        +isValidStartDate() bool
    }
    class TaskCreateInputBoundary {
        <<interface>>
        +execute(task: Task) CreatedTaskResponseModel
    }
    class TaskRegisterInteractor
    class TaskDsGateway {
        <<interface>>
        +existsByName(name: String) bool
        +save(model: TaskDsRequestModel)
    }
    class TaskPresenter {
        <<interface>>
        +prepareSuccessView(task: Task) CreatedTaskResponseModel
        +prepareFailView(error: String)
    }
    class JpaTaskDataProvider
    class TaskResponseFormatter
    TaskRegisterInteractor ..> Task : Use
    TaskCreateInputBoundary <|.. TaskRegisterInteractor : Implement
    TaskRegisterInteractor ..> TaskDsGateway : Use
    TaskRegisterInteractor ..> TaskPresenter : Use
    TaskDsGateway <|.. JpaTaskDataProvider : Implement
    TaskPresenter <|.. TaskResponseFormatter : Implement

Definição: Frameworks and Drivers — a camada mais externa

Camada onde ficam os detalhes técnicos de fato — o REST API do framework web, o driver de banco de dados, o servidor de aplicação. Segundo o próprio Robert C. Martin: "geralmente você não escreve muito código nesta camada além do código que possibilite a comunicação com as camadas mais internas do círculo" — ao usar o Spring Framework, por exemplo, essa camada já vem pronta. A regra de dependência é sempre a mesma: da camada mais externa para a mais interna, nunca o contrário.

Comparativo entre as quatro arquiteturas

Critério MVC Camadas Hexagonal Clean Architecture
Foco principal Separação apresentação/controle/modelo Organização em camadas sequenciais Isolamento do domínio via portas Camadas concêntricas isolando o domínio
Isolamento do domínio Fraco Fraco (acoplado a framework/persistência) Forte Muito forte
Testabilidade Média (controllers sobrecarregados dificultam) Baixa a média Alta Muito alta
Uso de interfaces/abstrações Pouco utilizado Fraco Fundamental (portas = interfaces) Fundamental (gateways e casos de uso via contrato)
Aderência ao DIP Fraca Não cumpre Forte Muito forte (uso extenso de inversão de dependência)
Complexidade Baixa a média Média Média Alta
Aplicação comum Aplicações web simples ou médias Sistemas pequenos, entrega rápida APIs, microsserviços, integrações externas Sistemas complexos, sustentáveis a longo prazo

Definição: Nenhuma arquitetura é sempre a certa

A tabela acima não elege uma "vencedora" — cada arquitetura troca simplicidade por isolamento/testabilidade em graus diferentes. Um sistema pequeno, de vida curta, pode não justificar a complexidade extra de uma Clean Architecture; um sistema grande e de longa duração, por outro lado, tende a pagar um preço alto (mais retrabalho, mais dificuldade de teste) se ficar preso a uma Arquitetura em Camadas simples demais. Domain-Driven Design — ver DDD — combina naturalmente com Hexagonal e Clean Architecture, aprofundando ainda mais a modelagem do domínio isolado.

Testes de arquitetura com ArchUnit

Definição: Teste de arquitetura

Além de depender de code review para garantir que um padrão arquitetural definido seja seguido por todos, é possível automatizar essa validação com testes unitários, usando bibliotecas como o ArchUnit — evitando que um padrão combinado pela equipe seja quebrado, sem querer, ao longo do tempo.

@AnalyzeClasses(packages = "br.com.brandao.tasks",
        importOptions = { ImportOption.DoNotIncludeTests.class, ImportOption.DoNotIncludeJars.class })
public class CleanArchitectureValidator {

    @ArchTest
    static final ArchRule objects_in_entity_package_should_not_use_objects_outside_entity_package =
        classes().that().resideInAPackage("..entity..")
            .should()
            .accessClassesThat()
            .resideInAPackage("..entity..")
            .because("Entity is the most inner layer, so it should depend on any other package");
}

A declaração de ArchRule, anotada com @ArchTest, valida — usando uma linguagem muito próxima da linguagem natural — se os padrões especificados (neste exemplo, que classes do pacote entity não dependam de nada fora dele) estão sendo respeitados a cada execução da suíte de testes.

Tiers x layers, objetos distribuídos e proxies

Camadas lógicas (layers) x camadas físicas (tiers)

Em português as duas palavras viram "camada", mas são conceitos diferentes:

Layer (camada lógica) Tier (camada física)
O que é Divisão do código em responsabilidades (apresentação, domínio, persistência) Divisão da execução em máquinas/processos separados
Comunicação Chamada de método local Chamada remota (rede), cara e sujeita a falhas
Objetivo Baixo acoplamento, mudanças localizadas Escalabilidade, disponibilidade, segurança, isolamento
Exemplo Controller → Service → Repository Navegador, servidor Web, servidor de aplicação, banco, cache

Uma aplicação pode ter três layers e um único tier (tudo no mesmo servidor), e tudo bem. Para os layers, veja Arquitetura em camadas.

Evolução dos tiers:

Modelo Como é Pontos fortes Problemas
1 tier Tudo no mesmo computador Simples Sem compartilhamento e sem escala
2 tiers (cliente-servidor) Cliente "gordo" (fat client) com lógica + banco Rápido de construir; usa stored procedures por segurança e desempenho Difícil atualizar todos os clientes; lógica no banco acopla ao fornecedor; escala e disponibilidade ruins
3 tiers Cliente "magro" (navegador) + servidor de aplicação + banco Evolução centralizada: sempre a última versão; mais seguro Latência extra (mais round-trips); mais componentes
N tiers Balanceador, servidores Web replicados, cache distribuído, banco com réplicas Escala e alta disponibilidade Complexidade de gestão, replicação de sessão, consistência

Duas métricas ao dividir em tiers: (1) número de round-trips: uma requisição do usuário não deveria gerar dezenas de chamadas internas; (2) tamanho do payload: serializar entidades inteiras desperdiça rede, e por isso surgem os DTOs com a granularidade certa (Backend). É o mesmo conselho que um DBA dá: poucas chamadas, buscando de uma vez só o que precisa.

Objetos distribuídos: "não distribua seus objetos"

Distribuir objetos em máquinas diferentes parece uma forma fácil de escalar (foi a promessa do RMI, do CORBA e dos EJBs remotos), mas a chamada remota é ordens de grandeza mais lenta e menos confiável que a local, e esconder isso atrás de uma interface "transparente" levou a projetos lentos e frágeis. Daí a Primeira Lei dos Objetos Distribuídos (Martin Fowler): não distribua seus objetos. Distribua só quando for inevitável (escala, isolamento), e então:

  • Use granularidade grossa: a operação remota entrega tudo que o cliente precisa numa só chamada (padrão Remote Facade), devolvendo DTOs (cópias serializáveis), não referências remotas.
  • Trate falhas, timeouts e retries explicitamente (a rede cai; veja System Design).
  • Prefira separar Web server e Application server (dois tiers) apenas quando houver razão de escala ou segurança.

Contexto histórico

Os EJBs (Enterprise JavaBeans) nasceram como evolução do RMI, oferecendo remotabilidade e serviços do contêiner (transações, segurança, ciclo de vida, pool): Stateful Session Beans (guardam estado de uma conversa, com passivação e ativação), Stateless (sem estado, de um pool, ideais para chamadas curtas) e Singleton (um por aplicação). A complexidade levou ao contêiner leve, defendido pelo Spring e depois padronizado pelo CDI: objetos locais com os mesmos serviços (DI, transação, segurança). Hoje a comunicação entre processos é feita por REST/gRPC ou mensageria.

Comunicação assíncrona (MOM)

Middleware orientado a mensagens (MOM) desacopla quem envia de quem recebe: o produtor entrega a mensagem a um broker e segue; o consumidor processa no seu ritmo. O modelo store-and-forward garante a entrega: o broker persiste a mensagem antes de confirmar. Em Java, o JMS padronizou a API (filas ponto a ponto e tópicos publicação/assinatura, com durable subscribers para não perder mensagens enquanto o consumidor está fora). O cuidado clássico é o contrato da mensagem: consumidores acoplados a um formato fixo quebram quando o produtor acrescenta um campo (veja leitor tolerante). Evolução moderna em Arquitetura orientada a eventos.

Escalar na nuvem não é mágica

A nuvem facilita escalar o hardware e a plataforma de forma elástica, mas não resolve sozinha: o banco de dados continua sendo o gargalo clássico. Opções (cada uma com trade-offs de consistência): réplicas de leitura, cache (dados potencialmente desatualizados), desnormalização (duplicar dados para ler mais rápido), particionamento/sharding e bancos NoSQL. Cuidados: dependência do provedor (lock-in) e da sua disponibilidade. Detalhes em Nuvem e NoSQL.

A escada do acoplamento: new → fábrica → locator → injeção

Como obter a implementação de uma dependência (por exemplo, o serviço de pagamentos)?

Abordagem Como Limite
new direto A classe instancia a implementação Acopla à classe concreta; difícil trocar e testar
Fábrica (Factory) Encapsula a escolha da implementação Passa a acoplar à fábrica; difícil usar implementações diferentes em pontos diferentes
Service Locator Registro central (por nome) de componentes Ponto de acesso global; a classe precisa saber onde buscar
Injeção de Dependências Quem monta o objeto entrega as dependências A classe conhece só a interface; o contêiner (Spring, CDI) decide

O Singleton clássico sofre o mesmo mal: o usuário acopla-se ao detalhe de que a classe é única (e em cluster "único" já não faz sentido). O mais saudável é que o contêiner garanta a instância única (@Singleton, escopo singleton do Spring) e a classe fique sem essa responsabilidade. Veja também Injeção de dependências e Singleton.

Proxies dinâmicos e geração de bytecode

Para esconder código de infraestrutura (acesso remoto, transação, segurança, cache, carregamento lazy) sem poluir o domínio, usa-se um proxy: um objeto que implementa a mesma interface e, a cada chamada, delega a outro acrescentando comportamento. Escrever um proxy à mão para cada classe é repetitivo. Em Java:

  • java.lang.reflect.Proxy + InvocationHandler: gera em memória, em tempo de execução, uma classe que implementa as interfaces dadas e encaminha todas as chamadas para o handler:
Empresa remota = (Empresa) Proxy.newProxyInstance(
    Empresa.class.getClassLoader(),
    new Class<?>[] { Empresa.class },
    (proxy, metodo, args) -> {
        System.out.println("antes: " + metodo.getName());   // ex.: abrir transação, medir tempo, ir pela rede
        Object resultado = metodo.invoke(alvo, args);
        System.out.println("depois");
        return resultado;
    });
  • Bibliotecas de manipulação de bytecode (CGLIB, Javassist, ByteBuddy) criam subclasses em tempo de execução (para classes sem interface). São a base do lazy loading do Hibernate, do @Transactional do Spring e do AOP (Spring AOP, AspectJ).

O custo é a "mágica": comportamento invisível no código, stack traces estranhos e limitações (classes final, chamadas internas this.metodo() que não passam pelo proxy). Prefira design por interfaces a abusar de reflexão e bytecode.

O que é um Design Pattern

As arquiteturas vistas acima organizam a estrutura de um projeto inteiro; os padrões a seguir atuam numa escala menor — a de classes e objetos individuais dentro de qualquer uma dessas camadas.

Definição: Design Pattern (padrão de projeto)

Uma solução reutilizável para um problema que se repete, em diferentes contextos, durante o projeto de um software orientado a objetos. A ideia vem do trabalho de Christopher Alexander em arquitetura de cidades e construções (A Pattern Language, 1977) — cada padrão descreve um problema e uma solução já testada com sucesso mais de uma vez. Não descreve soluções novas, mas soluções já consolidadas pela experiência de quem já passou por aquele problema antes.

Definição: GoF (Gang of Four)

Apelido dado aos quatro autores do livro fundador do catálogo de padrões — Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, 1994) — e, por extensão, ao próprio catálogo de 23 padrões que o livro descreve.

Definição: As três categorias do catálogo GoF

Os 23 padrões do livro do GoF são organizados em três categorias, de acordo com o tipo de problema que resolvem: Criação (problemas envolvendo a criação de objetos — Factory Method, Abstract Factory, Builder, Singleton), Estruturais (problemas relacionados à composição de classes e objetos numa estrutura maior — Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy) e Comportamentais (problemas sobre como objetos interagem e distribuem responsabilidades entre si — Strategy, Observer, State, Command, Chain of Responsibility, Mediator, Template Method, Visitor, entre outros). Essa classificação ajuda a navegar o catálogo, mas nenhum padrão existe "criado" do zero — todos vieram de observações e generalizações de soluções que já apareciam repetidamente na prática, antes mesmo de serem documentadas.

Definição: Padrão x anti-padrão

Uma solução recorrente só é um padrão se for uma boa solução — se o problema se repete mas a solução costumeira é ruim, o nome certo é anti-padrão. Conhecer os padrões não é só sobre aplicá-los: é também sobre reconhecer, ao ler o código de outra pessoa, que decisão de design está sendo usada e quais consequências ela traz.

Definição: Um padrão é mais do que um diagrama de classes

É comum reduzir um padrão só à sua estrutura (o diagrama UML de classes/interfaces) — mas isso é usar o padrão de forma incompleta. A estrutura de dois padrões pode ser bastante parecida, mas resolver problemas completamente diferentes; o que realmente importa reter de um padrão é o problema que ele resolve, o contexto onde ele se aplica bem, e as consequências (positivas e negativas) de usá-lo — não só a forma final do código.

Strategy: o primeiro padrão

Um sinal clássico de que um problema pede o padrão Strategy: uma classe cujo comportamento varia de acordo com uma regra de negócio (ex.: uma tarifa calculada diferente por tipo de cliente), inicialmente resolvida com uma cadeia de if/else que só cresce.

Evolução: condicional → herança → composição

Uma cadeia de condicionais que decide qual algoritmo aplicar não escala: toda regra nova é mais um if, e o método nunca para de crescer (o mesmo problema de coesão já visto no SRP).

A primeira tentativa de solução costuma ser herança: transformar o cálculo num método abstrato, com uma subclasse por regra. Isso resolve a explosão de ifs, mas introduz dois problemas novos:

  • Explosão de subclasses — uma combinação de critérios diferentes (tipo de veículo × período de cobrança, por exemplo) tende a multiplicar o número de subclasses necessárias, muitas delas duplicando código entre si.
  • Comportamento fixo na criação — como a subclasse é decidida no momento da instanciação (new), não é possível trocar o comportamento de um objeto já existente sem criar uma instância nova de outra classe.

A solução final é composição: extrair o algoritmo de cálculo para trás de uma interface própria, e a classe principal passa a delegar a execução para uma instância dessa interface, recebida (e trocável) por fora.

interface CalculoValor {
    double calcular(long periodo, Veiculo veiculo);
}

class CalculoDiaria implements CalculoValor {
    private double valorDiaria;

    CalculoDiaria(double valorDiaria) {
        this.valorDiaria = valorDiaria;
    }

    public double calcular(long periodo, Veiculo veiculo) {
        return valorDiaria * Math.ceil(periodo / HORA);
    }
}

class ContaEstacionamento {
    private CalculoValor calculo;
    // ...

    public double valorConta() {
        return calculo.calcular(fim - inicio, veiculo);
    }

    public void setCalculo(CalculoValor calculo) {
        this.calculo = calculo;
    }
}

Definição: Strategy

Padrão que deve ser usado quando uma classe precisa de diversos algoritmos que possam ser usados de forma intercambiável — a solução é delegar a execução do algoritmo para uma instância que compõe a classe principal (recebida via construtor ou setter), em vez de implementá-lo diretamente ou via herança. Como consequência, o algoritmo pode ser trocado sem alterar a classe principal, e até dinamicamente, em tempo de execução — a lógica condicional que decidia qual algoritmo usar desaparece da classe principal, empurrada para quem escolhe (e instancia) a estratégia certa.

classDiagram
    class Principal {
        +metodoPrincipal()
    }
    class Algoritmo {
        <<interface>>
        +executar()
    }
    class AlgoritmoConcreto {
        +executar()
    }
    Principal o--> Algoritmo
    Algoritmo <|.. AlgoritmoConcreto

Definição: Consequências negativas do Strategy

Nenhum padrão vem sem custo: o Strategy aumenta a complexidade de criação do objeto principal (a dependência precisa ser criada e configurada à parte — e, se o atributo ficar nulo por engano, o comportamento em tempo de execução é inesperado) e aumenta o número de classes do sistema (uma classe a mais para cada algoritmo), o que também tem um custo de gerenciamento. Avaliar as consequências negativas de um padrão contra o problema que ele resolve é parte de saber usá-lo bem — nenhum padrão é grátis.

Definição: Composição x agregação (nuance)

Neste contexto de padrões de projeto, "composição" é usada de forma mais geral, para indicar que uma classe guarda uma instância de outra como atributo — o mesmo sentido já usado em Boas Práticas. Formalmente, agregação é quando essa instância pode ser compartilhada entre vários objetos (existe independente do objeto que a referencia); composição (sentido estrito) é quando ela é exclusiva de um objeto (sua existência está atrelada à dele). Um Strategy costuma ser uma agregação (a mesma instância de CalculoValor pode ser compartilhada por várias contas) — mas, na prática do catálogo de padrões, os dois termos são frequentemente usados sem essa distinção fina.

Reúso por meio de herança

O potencial real de reúso da herança não está em reaproveitar a estrutura de dados da superclasse (isso qualquer composição resolve) — está em duas coisas: a superclasse poder chamar código que a subclasse define (é isso que os padrões desta seção exploram), e o polimorfismo permitir que código escrito contra o tipo da superclasse funcione com qualquer subclasse, sem modificação (o mesmo princípio por trás de Comparable e dos algoritmos de ordenação do Java).

Null Object

Um padrão simples que ataca um problema muito comum: código poluído de verificações if (objeto != null) repetidas para proteger contra um valor ausente.

Definição: Null Object

Em vez de retornar null quando não existe um valor válido, cria-se uma subclasse dedicada que estende o tipo original e implementa um comportamento neutro e seguro para cada método (ex.: devolver 0, uma string vazia, ou um link de login). Quem usa o objeto não precisa mais verificar se é nulo — chama os métodos normalmente, e a subclasse "nula" responde de um jeito que não quebra o resto do código.

class CarrinhoNulo extends Carrinho {
    public double getValor() { return 0.0; }
    public int getTamanho() { return 0; }
    public String getNomeUsuario() { return "<a href=login.jsp>Login</a>"; }
}

Quem consome Carrinho (via CookieFactory.criarCarrinho(request), por exemplo) para de precisar tratar o caso nulo — o próprio CarrinhoNulo já sabe responder de forma segura. Isso só funciona porque o código cliente desconhece completamente que implementação está usando: conhece só a interface/superclasse, e é o polimorfismo que decide, em tempo de execução, qual comportamento realmente roda.

Definição: Teste prático do LSP — a subclasse substitui a superclasse em todo contexto?

Uma forma direta de checar se uma relação de herança faz sentido: verificar se é possível substituir qualquer uso da superclasse por uma instância da subclasse, em qualquer contexto do programa, sem quebrar nada (é o Liskov Substitution Principle, na prática). Um exemplo clássico de dúvida real: estender JPanel numa tela de interface gráfica, mesmo sem usar boa parte do contrato de JPanel (adição de componentes, configuração de layout), quebra essa regra — só porque uma tela parece um painel visualmente não significa que ela deveria ser um painel, no sentido de herança. Quando a subclasse "finge" ser a superclasse só por conveniência (sem cumprir seu contrato inteiro), o sinal é de que a relação deveria ser outra (composição, por exemplo), não herança.

Hook methods

Definição: Hook method (método-gancho)

Técnica (mais geral que qualquer padrão específico) em que a superclasse define um método público que delega parte da sua execução para um método — o hook — que só é implementado pela subclasse. A superclasse fornece a estrutura geral; a subclasse "pendura" (daí o nome) sua lógica específica no ponto de extensão definido. É a técnica por trás de frameworks que pedem para a aplicação estender uma classe e implementar só alguns métodos — como a Servlet API do Java, em que HttpServlet.service() já existe pronto e chama doGet()/doPost(), que a aplicação implementa.

Definição: Modificadores de método e a intenção de design

Cada modificador comunica uma mensagem diferente sobre o papel de um método numa hierarquia: abstract diz "isto é um hook obrigatório, toda subclasse concreta precisa implementá-lo"; final diz "isto nunca pode ser um hook — o comportamento é fixo, imutável por subclasses"; private também nunca pode ser hook (só a própria classe o enxerga); protected/public sem final são candidatos naturais a hook method — inclusive com uma implementação vazia (hook opcional, que a subclasse só sobrescreve se precisar).

Definição: Hook method x Template Method — não são a mesma coisa

Hook method é a técnica geral (delegar parte da execução para um método que a subclasse define). Template Method é um padrão específico que usa essa técnica para resolver um problema mais delimitado (ver a seguir) — outros padrões também usam hook methods na sua solução, sem serem, por isso, Template Method.

Template Method

Definição: Template Method

Padrão indicado quando existe um algoritmo com estrutura fixa, mas alguns passos específicos variam — a mesma ideia de um modelo de documento (as seções fixas já vêm prontas; só os trechos específicos, como "Introdução" ou "Resultados", precisam ser preenchidos a cada uso). A superclasse implementa um método público (o "template") que coordena a execução, chamando, na ordem certa, um ou mais hook methods; cada subclasse concreta implementa só os hooks, preenchendo as partes variáveis do algoritmo sem alterar sua estrutura geral.

classDiagram
    class ClasseAbstrata {
        +metodoTemplate()
        #passoAlgoritmoA()
        #passoAlgoritmoB()
    }
    class ClasseConcreta {
        #passoAlgoritmoA()
        #passoAlgoritmoB()
    }
    ClasseAbstrata <|-- ClasseConcreta

O código cliente instancia a subclasse concreta, mas trabalha com ela através do tipo da superclasse (ClasseAbstrata c = new ClasseConcreta(); c.metodoTemplate();) — o método metodoTemplate() roda sempre igual, mas cada hook chamado dentro dele executa a versão específica daquela subclasse.

Definição: Outro cenário clássico — jobs assíncronos com fluxo comum

Além do exemplo de documento, um caso muito comum na prática: vários workers (tarefas assíncronas — envio de e-mail, importação de arquivo, busca periódica) compartilham o mesmo fluxo de execução (configurar → executar → tentar novamente em caso de falha, até um limite de tentativas), mas cada um com uma lógica de negócio diferente no passo "executar". O template cuida do fluxo comum (contador de tentativas, tratamento de TimeoutException); cada worker concreto só implementa o hook com sua lógica específica.

public abstract class TemplateWorker {
    private int limiteTentativas;

    public <T> T executar(Object parametros) {
        antesExecucao(parametros);
        T resultado = valorPadraoDeRetorno();
        int tentativas = 0;
        do {
            try {
                resultado = trabalhar(parametros);
            } catch (TimeoutException e) {
                tentativas++;
            }
        } while (deveContinuarTentando(tentativas));
        return resultado;
    }

    protected abstract <T> T trabalhar(Object parametros) throws TimeoutException;
    protected abstract <T> T valorPadraoDeRetorno();
    protected void antesExecucao(Object parametros) { }
    protected boolean deveContinuarTentando(int tentativas) { return false; }
}

Definição: Hook methods podem usar tipos genéricos

Nada impede que o método template (e seus hooks) sejam genéricos — cada worker concreto define, ao implementar trabalhar(), qual tipo T seu resultado assume, sem que a classe template precise conhecer esse tipo de antemão. Isso permite reaproveitar o mesmo fluxo de execução para workers que retornam objetos completamente diferentes entre si.

Factory Method

Uma classe que precisa de uma dependência, mas não sabe (nem deveria saber) qual implementação concreta usar — a decisão depende de contexto que só a subclasse tem.

Definição: Factory Method

Um hook method especializado em criar objetos: a superclasse define um método abstrato de criação (a "fábrica"), e cada subclasse o implementa devolvendo a instância concreta que faz sentido para ela. Isso permite que métodos gerais definidos na superclasse usem essa dependência sem nunca precisar conhecer sua classe concreta — só a abstração.

public abstract class ServicoAbstrato<E> {
    public abstract DAO<E> getDAO(); // Factory Method

    // método geral da superclasse, que usa a dependência sem conhecer sua classe concreta
    public void gravarEntidadeEmArquivo(Object id, String nomeArquivo) {
        E entidade = getDAO().recuperarPorId(id);
        // ...
    }
}

public class ServicoProduto extends ServicoAbstrato<Produto> {
    private DAO<Produto> dao;

    public DAO<Produto> getDAO() {
        if (dao == null) {
            dao = new ProdutoDAO(); // só aqui a classe concreta é conhecida
        }
        return dao;
    }
}
classDiagram
    class ClassePrincipal {
        +metodoFabrica()
        +metodoGeral()
    }
    class ClasseEspecifica {
        +metodoFabrica()
    }
    class Dependencia {
        <<interface>>
    }
    class ImplementacaoDependencia
    ClassePrincipal <|-- ClasseEspecifica
    ClasseEspecifica ..> ImplementacaoDependencia : cria
    Dependencia <|.. ImplementacaoDependencia

Definição: O ganho de desacoplamento do Factory Method

A superclasse (e qualquer método geral nela definido) fica desacoplada da criação da dependência — só a subclasse concreta conhece a implementação real. Trocar qual implementação é usada significa só trocar de subclasse (ou criar uma nova), sem tocar em nenhum método geral já escrito na superclasse.

Definição: Uma variação muito comum do nome 'Factory Method' na prática

É frequente encontrar o nome Factory Method aplicado a uma estrutura ligeiramente diferente da descrita acima: em vez de uma subclasse sobrescrevendo um hook method, o cenário é ter várias classes-fábrica independentes, cada uma implementando uma interface comum de criação, com o cliente recebendo (ou escolhendo) qual fábrica usar — por exemplo, uma FabricaElasticsearch e uma FabricaBanco, ambas implementando FabricaDeCriterio, cada uma configurando o produto à sua própria maneira. Estruturalmente, essa variação está mais para Static Factory Method combinado com Dependency Injection (ver mais adiante) do que para o Factory Method original do GoF — mas o nome "Factory Method" continua popular para descrevê-la, então vale reconhecer as duas formas ao ouvir o termo numa conversa ou entrevista.

Considerações do capítulo: herança bem usada

Os quatro padrões deste capítulo mostram facetas diferentes do mesmo princípio: o verdadeiro ganho da herança não é reaproveitar dados, é permitir que a superclasse chame código que só a subclasse define — Null Object aplica isso para representar "ausência de valor" de forma polimórfica; Hook Methods é a técnica de base; Template Method usa hooks para fixar a estrutura de um algoritmo enquanto varia seus passos; Factory Method usa um hook especificamente para desacoplar a criação de uma dependência da classe que a utiliza.

Delegando comportamento com composição

Herança só lida bem com uma dimensão de variação por vez. Quando um problema tem duas variações independentes ao mesmo tempo (por exemplo: o formato de um arquivo gerado — XML ou propriedades — combinado com um pós-processamento — compactado ou criptografado), tentar resolver as duas com herança leva a duplicação de código ou a uma explosão combinatória de subclasses (uma para cada combinação possível).

Bridge

Definição: Bridge

Padrão que separa duas variabilidades independentes de um problema em duas hierarquias de classes distintas, ligadas por composição — uma "ponte" entre elas. A classe Abstração guarda uma referência à interface que representa a segunda variabilidade; suas subclasses (AbstraçãoRefinada) especializam a primeira variabilidade sem precisar conhecer qual implementação da segunda está sendo usada. Qualquer combinação das duas hierarquias passa a ser possível sem duplicar código nem multiplicar classes — cada nova implementação de qualquer um dos dois lados se combina livremente com tudo que já existe do outro lado.

classDiagram
    class Abstracao {
        #Componente cmp
        +operacao()
        #executar()
    }
    class AbstracaoRefinada {
        #executar()
    }
    class Componente {
        <<interface>>
        +operacao()
    }
    class ComponenteA
    class ComponenteB
    Abstracao <|-- AbstracaoRefinada
    Abstracao o--> Componente
    Componente <|.. ComponenteA
    Componente <|.. ComponenteB
public abstract class GeradorArquivo {
    private PosProcessador processador; // a "ponte" para a segunda variabilidade

    public void setProcessador(PosProcessador processador) {
        this.processador = processador;
    }

    public final void gerarArquivo(String nome, Map<String, Object> propriedades)
            throws IOException {
        String conteudo = gerarConteudo(propriedades); // hook: primeira variabilidade
        byte[] bytes = conteudo.getBytes();
        bytes = processador.processar(bytes); // delega a segunda variabilidade
        // ... grava bytes no arquivo
    }

    protected abstract String gerarConteudo(Map<String, Object> propriedades);
}

GeradorArquivo (e suas subclasses GeradorXML/GeradorPropriedades) representa uma variabilidade — o formato; PosProcessador (com implementações Compactador/ Criptografador) representa a outra — o pós-processamento. Qualquer formato pode ser combinado com qualquer pós-processamento, escolhido em tempo de execução via setProcessador.

Definição: Efeito colateral do Bridge — reúso em outros contextos

Ao separar uma responsabilidade (o pós-processamento) numa hierarquia própria e desacoplada, essa hierarquia frequentemente fica reutilizável em contextos que nada têm a ver com o problema original — o mesmo Compactador/Criptografador criado para gerar arquivos poderia, por exemplo, ser reaproveitado para processar dados antes de enviá-los pela rede.

Definição: Composição não é 'sempre melhor' que herança

Um alerta recorrente na comunidade é "prefira sempre composição a herança" — mas tratar isso como regra absoluta é um exagero. Não existe bala de prata em design de software: composição tem vantagens reais (as exploradas neste capítulo), mas a escolha certa depende sempre do problema e dos requisitos específicos, não de uma regra aplicada sem pensar.

Hook classes: whitebox x blackbox framework

Definição: Whitebox framework x blackbox framework

Whitebox (caixa branca) é um framework estendido via herança — a subclasse precisa conhecer a estrutura interna da superclasse (seus hook methods) para saber onde inserir comportamento; qualquer método público/protegido não-final é um ponto de extensão em potencial. Blackbox (caixa preta) é um framework estendido via composição — quem estende só precisa implementar a interface esperada, sem conhecer nada da estrutura interna da classe principal. Blackbox é mais simples de usar (não exige entender o interior da classe), mas mais difícil de identificar de início (é preciso já saber qual porção do comportamento vale a pena extrair como interface própria).

Definição: Hook class

Quando um ponto de extensão via composição (blackbox) é identificado e formalizado — a interface que representa aquela variabilidade — a classe que a implementa é chamada de hook class. É comum uma classe começar com hook methods (whitebox, mais simples de introduzir cedo) e, conforme os pontos de extensão mais usados ficam claros, ser refatorada para hook classes (blackbox) — não é incomum, nem incorreto, um framework combinar as duas abordagens.

State

Outro cenário recorrente: uma entidade cujo comportamento muda de acordo com seu estado interno (uma conta corrente se comporta diferente com saldo negativo; um personagem de jogo reage diferente conforme seu estado). Resolver isso com condicionais que checam o estado atual, espalhados pela classe, tende a ficar confuso rapidamente — e cada estado novo exige alterar código já existente.

Definição: State

Usa composição para representar cada estado possível como uma classe própria, todas implementando uma abstração comum. A entidade guarda uma referência a essa abstração (o estado atual) e delega para ela qualquer comportamento dependente do estado — quando o estado muda, basta trocar qual instância a entidade referencia, e todo o comportamento muda junto, sem nenhum condicional na classe principal.

classDiagram
    class Entidade {
        -Estado estado
        +metodoNegocio()
    }
    class Estado {
        <<interface>>
        +operacaoDependenteDoEstado()
    }
    class EstadoA {
        +operacaoDependenteDoEstado()
    }
    class EstadoB {
        +operacaoDependenteDoEstado()
    }
    Entidade o--> Estado
    Estado <|.. EstadoA
    Estado <|.. EstadoB

Definição: Consequências do State

Positivo: adicionar um estado novo, ou alterar um existente, não exige tocar nos outros estados nem na classe principal — cada estado é isolado na sua própria classe. Negativo: como a lógica fica dividida entre várias classes, fica mais difícil ter uma visão global de todos os estados e das transições possíveis entre eles, só olhando para uma classe.

Definição: Uma implementação mais completa — o estado sabe sua própria transição

Numa versão mais rica do padrão, cada método da interface Estado devolve qual é o próximo estado (em vez de retornar void) — a entidade não decide para qual estado ir, só delega a ação e atualiza sua referência com o retorno. Isso concentra o conhecimento sobre transições exatamente onde ele faz mais sentido: dentro de cada estado, não espalhado pela entidade principal.

public interface Estado {
    Estado pegarFlorDeGelo();
    Estado levarDano();
}

public class Pequeno implements Estado {
    public Estado pegarFlorDeGelo() { return new FlorDeGelo(); }
    public Estado levarDano() { return this; } // sem poderes, não há como perder mais
}

public class FlorDeGelo implements Estado {
    public Estado pegarFlorDeGelo() { return this; } // já tem o poder, nada muda
    public Estado levarDano() { return new Pequeno(); }
}
public class Personagem {
    private Estado estadoAtual = new Pequeno();

    public void pegarFlorDeGelo() {
        estadoAtual = estadoAtual.pegarFlorDeGelo();
    }

    public void levarDano() {
        estadoAtual = estadoAtual.levarDano();
    }
}

A classe principal (Personagem) fica reduzida a delegar cada ação e guardar o resultado — nenhum if decidindo qual é o próximo estado aparece nela, e adicionar um estado novo significa só implementar Estado mais uma vez, sem alterar Personagem.

Definição: enum como implementação de State

Um enum Java pode implementar métodos (inclusive abstratos, sobrescritos por cada constante) — o que permite usar um enum diretamente como implementação do padrão State, sem precisar de uma hierarquia de classes/interface separada. Funciona bem quando o conjunto de estados é fixo e conhecido de antemão. Deixa de funcionar bem quando: (1) o conjunto de estados precisa ser extensível por quem usa a classe (um enum não pode ganhar constantes novas fora do próprio arquivo onde é declarado), ou (2) cada instância do estado precisaria guardar um dado específico daquele objeto (as constantes de um enum são compartilhadas — singletons — entre todos que usam aquele estado, então não há como um mesmo estado guardar um valor diferente por instância da entidade).

Definição: Repetição de condicionais como sinal de refatoração — State x Strategy

Encontrar a mesma estrutura de condicionais repetida em vários pontos de uma classe é um sinal de que vale a pena refatorar para um padrão baseado em composição — qual dos dois depende do motivo da variação: se o comportamento muda conforme um estado interno do próprio objeto, o caminho é State; se o que varia é a implementação de um algoritmo escolhida por configuração/contexto externo (não pelo estado do objeto em si), o caminho é Strategy.

Definição: Duas perguntas práticas para diferenciar Strategy de State

  • Strategy: uma vez definida, a estratégia muda durante aquela execução? Se a resposta é normalmente não, mas os ifs tendem a se espalhar pelo código toda vez que uma implementação nova é escolhida em algum contexto, o padrão é Strategy.
  • State: o código cliente precisa ficar verificando qual é o estado atual o tempo todo, antes de decidir uma ação? Se sim, essa é a dor que o State resolve — cada estado passa a saber sozinho como reagir e para qual estado ir, e a verificação explícita desaparece do código cliente.

Observer

Um cenário muito comum: várias classes diferentes precisam saber quando algo muda num objeto (uma carteira de ações sendo atualizada, um componente gráfico que precisa se redesenhar, um log que precisa registrar o evento) — sem que esse objeto precise conhecer, uma por uma, todas as classes interessadas nele.

Definição: Observer

Padrão em que um objeto observável mantém uma lista de observadores registrados (todos implementando uma interface comum) e, sempre que seu estado muda, notifica todos eles através de um método definido nessa interface. O observável não precisa conhecer as classes concretas dos observadores — só a interface — o que permite adicionar ou remover observadores em tempo de execução, sem alterar o observável.

classDiagram
    class Observavel {
        +adicionarObservador()
        +removerObservador()
        +notificar()
    }
    class ObservavelConcreto {
        -estado
        +mudarEstado()
    }
    class Observador {
        <<interface>>
        +atualizar()
    }
    class ObservadorA
    class ObservadorB
    Observavel <|-- ObservavelConcreto
    Observavel o--> Observador
    Observador <|.. ObservadorA
    Observador <|.. ObservadorB
public interface Observador {
    void mudancaQuantidade(String acao, Integer qtd);
}

public class CarteiraAcoes {
    private Map<String, Integer> acoes = new HashMap<>();
    private List<Observador> obs = new ArrayList<>();

    public void adicionaAcoes(String acao, Integer qtd) {
        // ... atualiza o mapa de ações
        notificar(acao, qtd);
    }

    private void notificar(String acao, Integer qtd) {
        for (Observador o : obs) {
            o.mudancaQuantidade(acao, qtd);
        }
    }

    public void addObservador(Observador o) {
        obs.add(o);
    }
}

Cada classe interessada (um logger, um gráfico de barras, uma auditoria) implementa Observador e se registra via addObservador — nenhuma delas precisa que CarteiraAcoes conheça sua existência de antemão.

Definição: Consequência principal do Observer — desacoplamento

A classe observada e as observadoras ficam desacopladas: o mesmo observador pode receber notificações de vários objetos diferentes, e vários observadores diferentes podem reagir ao mesmo objeto — sem que nenhum lado conheça a implementação concreta do outro.

Definição: Variações comuns do Observer

O padrão admite bastante variação prática: notificações diferentes para eventos diferentes (cada uma com seu próprio método na interface, em vez de um único atualizar() genérico); parâmetros passados no momento do registro (via construtor) versus por método de exclusão separado; e a decisão de rodar cada notificação numa thread própria, quando a demora de um observador não deveria atrasar os demais.

Definição: Observer nas APIs Java — onde você já usou sem saber

O ActionListener do Swing (JButton.addActionListener(...)) é um Observer: o botão é o observável, o listener é o observador. O MessageListener do JMS (Java EE), os listeners de sessão de Servlets, e os listeners de mudança de valores em entidades JPA seguem o mesmo padrão — listener é só outro nome popular para "observador" nessas APIs. A JDK também tem, desde a versão 1.0, uma interface java.util.Observer e classe java.util.Observable prontas — mas elas são pouco usadas na prática (e hoje consideradas legadas): a maioria das APIs prefere implementar sua própria versão do padrão, adaptada ao contexto específico, a depender de uma solução genérica pronta.

Encerrando: composição como ferramenta de extensão

Os três padrões deste capítulo mostram motivos diferentes para delegar comportamento a outro objeto via composição: Strategy delega a execução de um algoritmo que pode ser trocado; State delega o comportamento que depende do estado interno do objeto; Observer delega, para várias outras classes ao mesmo tempo, a reação a mudanças ocorridas num objeto. Bridge mostra que composição e herança não são mutuamente exclusivas — podem ser combinadas na mesma solução, cada uma resolvendo uma parte diferente do problema.

Composição recursiva

Definição: Composição recursiva

Uma classe pode ter, como atributo, uma referência à própria abstração dela mesma (sua superclasse ou uma interface que ela implementa) — permitindo que uma instância seja composta por outras instâncias do mesmo tipo geral, formando uma estrutura recursiva (o mesmo princípio por trás de listas ligadas, árvores e grafos, vistos em Estrutura de Dados). Diferente de uma struct recursiva em C, o diferencial aqui é o polimorfismo: o tipo base pode ser uma interface ou superclasse com múltiplas implementações diferentes, cada uma podendo, por sua vez, ser estendida livremente.

Composite

Um erro comum ao modelar orientado a objetos: usar herança para representar "um conjunto de X" como se fosse um tipo de X (uma cesta de maçãs não é uma maçã) — mas às vezes um conjunto realmente deveria ser tratado como um indivíduo só (um kit de produtos vendido como produto único, com seu próprio preço).

Definição: Composite

Padrão que permite tratar um objeto simples e um conjunto desses objetos através da mesma abstração — ambos implementam a mesma interface, então o código cliente não precisa saber se está lidando com um ou com muitos. A classe "composta" implementa cada operação delegando e combinando o resultado das instâncias que a compõem (que podem ser simples, ou compostas novamente — daí a estrutura de árvore).

classDiagram
    class Abstracao {
        <<interface>>
        +operacao()
    }
    class Simples {
        +operacao()
    }
    class Composto {
        +operacao()
    }
    Abstracao <|.. Simples
    Abstracao <|.. Composto
    Composto o--> Abstracao
public interface TrechoAereo {
    String getOrigem();
    String getDestino();
    double getPreco();
}

public class TrechoSimples implements TrechoAereo {
    private String origem, destino;
    private double preco;
    // getOrigem/getDestino/getPreco retornam os atributos diretamente
}

public class TrechoComposto implements TrechoAereo {
    private TrechoAereo primeiro, segundo;
    private double taxaConexao;

    public TrechoComposto(TrechoAereo primeiro, TrechoAereo segundo, double taxaConexao) {
        this.primeiro = primeiro;
        this.segundo = segundo;
        this.taxaConexao = taxaConexao;
        if (!primeiro.getDestino().equals(segundo.getOrigem())) {
            throw new RuntimeException("O destino do primeiro não é igual a origem do segundo");
        }
    }

    public String getOrigem() { return primeiro.getOrigem(); }
    public String getDestino() { return segundo.getDestino(); }
    public double getPreco() {
        return primeiro.getPreco() + segundo.getPreco() + taxaConexao;
    }
}

Um TrechoComposto pode ser composto por dois TrechoSimples, ou por outro TrechoComposto (representando uma escala com múltiplas conexões) — o cliente que consulta getPreco() nunca precisa saber quantos trechos existem por trás, só que recebe um TrechoAereo válido.

Definição: Folhas x ramos numa estrutura Composite

Numa estrutura Composite, as implementações "simples" (sem composição) são as folhas da árvore resultante; as implementações "compostas" são os ramos, que delegam e combinam os filhos que as compõem. O número de filhos de um ramo varia livremente conforme a necessidade (dois, no exemplo de trechos aéreos; um número qualquer, em outros contextos).

Definição: Composite em componentes gráficos

Uso muito comum do padrão: um componente de interface gráfica (um formulário, uma janela) frequentemente é composto por outros componentes, mas tratado como um componente único — ao chamar setEnable() no componente pai, ele coordena a chamada em todos os filhos internos. Frameworks de UI como Swing, SWT e componentes de página JSF seguem essa estrutura.

Definição: Por que não simplesmente herdar de TrechoSimples?

Poderia parecer mais rápido fazer TrechoComposto extends TrechoSimples em vez de criar uma interface comum — mas isso obrigaria TrechoComposto a herdar atributos e um construtor que não fazem sentido para ele (ele não tem origem/destino/preço fixos, tem dois trechos internos). É o code smell Refused Bequest (herança recusada, já visto em Boas Práticas): quando a herança é dada a uma classe, mas ela não usa a estrutura/métodos herdados como o pai pretendia, é sinal de que a relação certa não é herança.

Chain of Responsibility

Um cenário recorrente: uma funcionalidade precisa executar vários passos em sequência, onde é comum precisar reordenar os passos, adicionar um novo, remover um existente, ou reaproveitar passos individuais em outros fluxos.

Definição: Chain of Responsibility

Padrão que organiza uma sequência de passos como uma cadeia de execução: cada elemento processa a informação e decide se delega para o próximo da cadeia. Na forma tradicional, os elementos são percorridos até que um deles trate a requisição, encerrando ali (útil, por exemplo, para buscar um recurso em fontes cada vez mais custosas — memória, depois banco, depois um servidor remoto, parando assim que alguma fonte encontrar o valor). Numa variação, todos os elementos executam sua lógica até a cadeia terminar, ou até um deles finalizar explicitamente a execução dos demais.

public abstract class RecuperadorArquivo {
    private RecuperadorArquivo proximo;

    public RecuperadorArquivo(RecuperadorArquivo proximo) {
        this.proximo = proximo;
    }

    public Arquivo recuperar(String nome) {
        Arquivo a = recuperaArquivo(nome);
        if (a == null || !a.isValido())
            return chamarProximo(nome);
        else
            return a;
    }

    protected Arquivo chamarProximo(String nome) {
        if (proximo == null)
            throw new RuntimeException("Não foi possível recuperar o arquivo");
        return proximo.recuperar(nome);
    }

    protected abstract Arquivo recuperaArquivo(String nome); // hook method
}

Cada subclasse concreta (RecuperadorCacheMemoria, RecuperadorCacheBanco, RecuperadorRemoto) implementa só recuperaArquivo, buscando numa fonte específica; recuperar (o "template") já cuida de tentar a fonte local e, se necessário, delegar para o próximo elemento da cadeia.

classDiagram
    class ElementoCadeia {
        -proximo
        +executar()
        #executarElemento()
    }
    class ElementoA {
        #executarElemento()
    }
    class ElementoB {
        #executarElemento()
    }
    ElementoCadeia <|-- ElementoA
    ElementoCadeia <|-- ElementoB
    ElementoCadeia o--> ElementoCadeia : proximo

Definição: Chain of Responsibility é Template Method + Composição Recursiva

É comum um padrão ser, na prática, a combinação de mais de um padrão mais simples — aqui, recuperar() funciona como um Template Method (lógica comum + hook recuperaArquivo implementado pelas subclasses), enquanto o encadeamento em si (cada elemento com uma referência a um "próximo" do mesmo tipo) é composição recursiva. Reconhecer padrões menores compostos dentro de um padrão maior é comum — e às vezes difícil de perceber à primeira vista.

Definição: Ganhos do Chain of Responsibility

Flexibilidade para reordenar, adicionar ou remover elementos da cadeia sem alterar o código dos outros elementos — inclusive em tempo de execução, conforme o contexto (ex.: excluir da cadeia um elemento de cache que não faz sentido num dispositivo com pouca memória RAM).

Definição: Filtros em aplicações Web são Chain of Responsibility

A interface Filter da API Java EE (Servlets) é um exemplo direto do padrão: cada filtro implementa doFilter(), decidindo se processa a requisição e delega para o próximo filtro da cadeia (ou para o recurso final) — a mesma estrutura de "processa e delega ao próximo", só que aplicada a requisições HTTP em vez de busca de arquivos.

Envolvendo objetos

Encapsulamento e polimorfismo, juntos, permitem uma técnica poderosa: uma classe pode envolver uma instância do mesmo tipo que ela implementa, sem que o código cliente perceba — servindo de intermediária entre quem chama e o objeto real, com controle total sobre a execução antes e depois de cada chamada de método (podendo até interceptar, alterar o retorno, ou nem chamar o método original).

Proxy e Decorator: mesma estrutura, motivações diferentes

Um sintoma clássico que motiva especificamente o Decorator: uma hierarquia de herança que tenta representar combinações de características (uma arma que pode ser mágica, flamejante, ou as duas coisas ao mesmo tempo) — cada combinação nova exige uma subclasse nova (ArmaMagica, ArmaFlamejante, ArmaMagicaFlamejante, ...), numa explosão combinatória que só piora conforme mais características são adicionadas.

Definição: Proxy x Decorator — a diferença é de intenção, não de estrutura

Os dois padrões compartilham a mesma estrutura (composição recursiva: uma classe que implementa uma abstração e encapsula outra instância da mesma abstração), o que muda é o motivo de usar cada um. Decorator existe para adicionar funcionalidade a um objeto já existente, de forma transparente (a analogia clássica: uma moldura "decorando" um quadro, sem alterar o quadro em si). Proxy existe para servir como intermediário protetor/controlador de acesso a um objeto principal — tipicamente um objeto remoto ou caro de criar — e é sempre citado neste livro pensando nessa proteção específica.

classDiagram
    class Abstracao {
        <<interface>>
        +operacao()
    }
    class Decorator {
        +operacao()
    }
    class Proxy {
        +operacao()
    }
    class Implementacao {
        +operacao()
    }
    Abstracao <|.. Decorator
    Abstracao <|.. Proxy
    Abstracao <|.. Implementacao
    Decorator o--> Abstracao
    Proxy o--> Implementacao

Definição: A diferença estrutural sutil entre Proxy e Decorator

Um Decorator costuma receber o objeto encapsulado por fora (construtor ou configuração), aceitando qualquer implementação daquela abstração — o foco é a funcionalidade adicionada, não o objeto específico. Um Proxy, muitas vezes, protege um objeto específico, e a criação desse objeto pode acontecer dentro do próprio Proxy; no caso de acesso remoto, ele pode nem compor a classe original diretamente, e sim classes de acesso à rede que delegam as chamadas por trás. Essa diferença é sutil, e às vezes tratada como um detalhe de implementação — o motivo é sempre o que mais importa para escolher entre os dois.

Definição: Ambos podem ser encadeados (como Chain of Responsibility)

Como Proxy e Decorator usam composição recursiva, é possível encadear vários deles — um Proxy (ou Decorator) pode encapsular outro Proxy/Decorator, que encapsula o objeto original — cada camada assumindo uma responsabilidade diferente, sem que o cliente perceba quantas camadas existem por trás da interface que ele usa.

Definição: Crie uma interface mesmo quando 'nunca vai precisar de outra implementação'

Um erro comum é não criar uma interface para uma classe só porque parece que ela nunca terá uma segunda implementação — mas ter uma abstração é o que possibilita envolvê-la depois com um Proxy (para cache, validação, controle de acesso, ...) sem tocar no código cliente. A maioria das IDEs modernas (Eclipse, IntelliJ) tem uma refatoração "extrair interface" automatizada — criar a interface cedo tem custo baixo e mantém a porta aberta para essa técnica depois.

Cenários clássicos de Proxy

  • Proteção de acesso — validar parâmetros ou bloquear um método antes de ele executar, mantendo essa lógica fora da classe de negócio. Um uso concreto: um Proxy que sanitiza/valida entradas para evitar ataques de injeção.

Definição: Ataque de injeção (SQL Injection, XSS)

Ataque que explora concatenação insegura de strings para alterar o comando que a aplicação pretendia executar. SQL Injection injeta partes de SQL num parâmetro para alterar a consulta executada no banco; Cross-Site Scripting (XSS) injeta JavaScript num parâmetro usado na construção de uma página web.

  • Cache de execução — armazenar o resultado de uma chamada demorada e devolvê-lo em chamadas futuras, sem executar a lógica de novo — mantendo essa responsabilidade fora da classe de negócio (que só sabe calcular, não sabe que está sendo cacheada).
  • Acesso remoto — encapsular a comunicação de rede com outro objeto (outro servidor, outro processo), fazendo o objeto remoto parecer local para quem o usa.
  • Criação tardia de objetos caros — adiar a criação de um objeto custoso até o momento em que ele realmente for necessário, escondendo esse adiamento atrás da mesma interface do objeto real.

Definição: Proxy nas APIs do Java — onde você já usou sem saber

  • Collections.synchronizedList() / unmodifiableList() — encapsulam a lista original num Proxy que adiciona sincronização, ou bloqueia modificação, respectivamente, sem que o código cliente perceba a diferença.
  • RMI (Remote Method Invocation) — ao invocar um método remoto, o cliente chama um stub (um Proxy local que representa o objeto remoto); do lado do servidor, um skeleton recebe a chamada e a repassa ao objeto real.
  • Carregamento preguiçoso (lazy loading) do JPA — quando uma entidade tem uma associação configurada para carregar sob demanda (ex.: a lista de telefones de uma Pessoa), o JPA popula esse atributo com um Proxy, que só consulta o banco de dados na primeira vez que a lista é efetivamente acessada — não no momento em que a Pessoa é carregada.

Adapter

Um cenário diferente dos dois anteriores: existe uma classe já pronta, mas ela implementa uma interface diferente da que o restante do código espera — o exemplo físico clássico é o adaptador de tomada, que traduz um formato de plugue para outro sem que o aparelho por trás precise mudar.

Definição: Adapter

Padrão que faz uma classe existente (Adaptada) ser usável através de uma interface diferente (InterfaceAlvo) da que ela já implementa — o Adaptador implementa a interface esperada pelo cliente e, por dentro, traduz cada chamada para o método correspondente da classe adaptada.

classDiagram
    class Cliente
    class InterfaceAlvo {
        <<interface>>
        +requisicao()
    }
    class Adaptador {
        +requisicao()
    }
    class Adaptada {
        +requisicaoDiferente()
    }
    Cliente ..> InterfaceAlvo
    InterfaceAlvo <|.. Adaptador
    Adaptador o--> Adaptada
public class Adaptador implements InterfaceAlvo {
    private Adaptada adaptada;

    public void requisicao() {
        adaptada.requisicaoDiferente();
    }
}

Definição: O que diferencia Adapter de Proxy/Decorator, estruturalmente

Nos três padrões, uma classe encapsula outra — a diferença central é que, no Adapter, a classe encapsulada (Adaptada) não implementa a mesma interface que a classe que a envolve. É justamente essa diferença de interface que motiva o Adapter a existir: ele é a "tradução" entre duas abstrações incompatíveis.

Definição: Tradução nunca é totalmente trivial

Adaptar uma interface para outra raramente é só renomear métodos — é comum precisar reconciliar diferenças de parâmetros (um atributo passado como parâmetro numa API pode ser parte do construtor na outra), de formato de dado (uma mensagem de texto único dividida em vários trechos de tamanho fixo), e de tratamento de erro (uma API lança exceção, a outra retorna um booleano). Casos mais complexos, com diferenças semânticas profundas entre as APIs, podem até exigir a ajuda de um especialista no domínio para entender a tradução correta.

Definição: Preserve a exceção original ao adaptar erros

Ao traduzir uma exceção lançada pela API adaptada para o tipo esperado pela interface alvo, é importante passar a exceção original como causa da nova (o parâmetro cause do construtor de Throwable, em Java) — sem isso, o stacktrace da exceção nova aponta só para dentro do próprio Adapter, escondendo a causa raiz real do problema.

Definição: Adapter para migração de versões de API

Um uso muito comum do Adapter: dar suporte a código já escrito contra uma versão antiga de uma API, quando ela evolui para uma interface nova incompatível — um Adapter traduz entre as duas, permitindo que código legado continue funcionando sem reescrita imediata. O exemplo clássico em Java: a interface Enumeration (JDK 1.0) foi substituída por Iterator (JDK 1.2, métodos com nomes mais curtos e um remove() opcional) — a biblioteca Apache Commons Collections oferece IteratorUtils.asIterator()/asEnumeration() para adaptar de uma para a outra nos dois sentidos.

Definição: Adapter para unificar múltiplos sistemas legados

Outro cenário real e comum: várias fontes de dados legadas (uma API SOAP, uma fila de mensagens, um banco NoSQL), cada uma com seu próprio formato e protocolo, precisando ser apresentadas ao restante da aplicação através de uma única interface simples. Cada fonte legada ganha seu próprio Adapter, que sabe traduzir suas peculiaridades (XML de uma API SOAP, por exemplo) para o contrato comum esperado pelo cliente — de forma que o dia em que aquele sistema legado for finalmente aposentado, só o Adapter correspondente precisa mudar, não o restante do código. Essa é essencialmente a mesma ideia do Anti-Corruption Layer, aplicada uma fonte de dados por vez em vez de um sistema inteiro.

Estratégias de criação de objetos

Construtores parecem simples, mas têm três limitações que motivam boa parte dos padrões de criação:

  • Não podem ter dois construtores com parâmetros do mesmo tipo — como todo construtor tem obrigatoriamente o mesmo nome da classe, não há como usar um nome expressivo para diferenciá-los (ex.: uma classe CoordenadaGeografica não consegue ter um construtor que recebe uma string no formato Geodésico e outro no formato Geodésico Decimal, já que ambos receberiam só uma String como parâmetro).
  • Um construtor sempre cria um objeto novo — não é possível fazer um construtor devolver uma instância já existente, o que inviabiliza reaproveitar objetos sem estado próprio ou manter uma única instância compartilhada de algo.
  • Um construtor só pode retornar objetos da própria classe — nunca uma subclasse, nem o objeto envolvido por um Proxy.

Simple Factory

Antes de partir para os padrões formais de criação, vale conhecer a solução mais direta possível: quando a lógica de decidir e construir um objeto (mesmo que só um tipo de produto) começa a se espalhar em if/else pelo código cliente, o primeiro passo quase sempre é extrair essa lógica para uma classe própria.

Definição: Simple Factory

Uma classe dedicada só a criar instâncias de um tipo de produto, escondendo do cliente qualquer lógica de decisão envolvida (qual if levou a qual configuração, por exemplo). Resolve o problema de espalhar if/else de criação pelo código, mas não é considerado um padrão por boa parte dos autores (incluindo o catálogo GoF) — é tratado antes como um idioma de linguagem, por ser simples demais para carregar o peso de "padrão de projeto". Ainda assim, é a solução mais pragmática quando existe só uma fábrica e um tipo de produto — não há necessidade de evoluir direto para Factory Method ou Abstract Factory se o problema não pede a flexibilidade extra que eles trazem.

classDiagram
    class Cliente
    class SimpleFactory {
        +criar(parametros)
    }
    class Produto
    Cliente ..> SimpleFactory
    SimpleFactory ..> Produto : cria

A refatoração típica para chegar até aqui é a mesma já vista em Boas Práticas: Extrair Classe para tirar a responsabilidade de criação da classe original, seguida de Mover Método para levar a lógica de decisão (os ifs) para dentro da fábrica nova — o cliente original passa a só chamar um método e receber o produto já pronto, sem saber como ele foi montado.

Definição: Quando Simple Factory deixa de ser suficiente

Continua sendo a melhor escolha enquanto existir só uma forma de criar aquele tipo de produto. No momento em que aparece a necessidade de várias fábricas diferentes — cada uma com sua própria lógica de configuração para o mesmo tipo de produto —, é sinal de que vale a pena evoluir para Factory Method.

Static Factory Method

Definição: Static Factory Method

A solução mais simples para as limitações acima: em vez de expor o construtor, a classe delega a criação de instâncias a um método estático, que os clientes chamam no lugar de new. Apesar de amplamente usado, não é um padrão do catálogo GoF — é descrito com detalhe no livro Effective Java, de Joshua Bloch. Ele resolve as três limitações de uma vez: o nome do método pode ser tão expressivo quanto necessário (criarGeodesico() x criarGeodesicoDecimal(), em vez de dois construtores indistinguíveis), pode devolver uma instância já existente em vez de sempre criar uma nova, e pode devolver qualquer subtipo compatível com o tipo declarado — inclusive um Proxy.

public abstract class FabricaGerador {
    public static GeradorArquivo criarGeradorXML(String... processadores) {
        GeradorArquivo g = new GeradorXML();
        g.setProcessador(criarProcessador(processadores));
        return g;
    }

    public static GeradorArquivo criarGeradorPropriedades(String... processadores) {
        GeradorArquivo g = new GeradorPropriedades();
        g.setProcessador(criarProcessador(processadores));
        return g;
    }
    // ...
}
GeradorArquivo ga = FabricaGerador.criarGeradorXML(FabricaGerador.ZIP, FabricaGerador.CRYPTO);

A classe cliente só conhece FabricaGerador e a abstração GeradorArquivo — as subclasses concretas (GeradorXML, GeradorPropriedades) ficam encapsuladas dentro da fábrica. A própria API do Java usa essa técnica com frequência: Integer.valueOf() e Integer.parseInt() fazem cache de valores pequenos para evitar criar instâncias repetidas do mesmo número.

Definição: Static Factory Method x Factory Method — não confundir

O nome parecido é fonte comum de confusão. Static Factory Method é um método estático usado para encapsular a criação de instâncias por parte da classe cliente. Factory Method (visto no capítulo de reúso por herança) é um hook method definido por uma superclasse para que suas subclasses decidam qual implementação instanciar — não é estático, e o mecanismo de variação é herança, não um método utilitário.

Definição: Impedindo a invocação direta do construtor

Disponibilizar um Static Factory Method não impede, por si só, que o código cliente continue chamando o construtor diretamente — para isso, o construtor precisa ter sua visibilidade reduzida. Se o método fábrica está na mesma classe que está sendo criada, o construtor pode ser private. Se estiver em outra classe, uma solução é declará-lo protected (ou de pacote) e colocar a fábrica no mesmo pacote das classes que ela cria.

Um único objeto da classe com Singleton

Definição: Singleton

Caso especial de Static Factory Method para quando a aplicação deve ter apenas uma instância de uma determinada classe (ex.: a configuração global do sistema, ou o tabuleiro de um jogo de xadrez entre duas pessoas). O construtor é private; um atributo estático guarda a única instância; e um método estático público (getInstancia()) a cria na primeira chamada e a devolve em todas as seguintes.

public class Configuracao {
    private static Configuracao instancia;

    public static Configuracao getInstancia() {
        if (instancia == null) {
            instancia = new Configuracao();
        }
        return instancia;
    }

    private Configuracao() {
        // lê as configurações
    }
}
Configuracao c = Configuracao.getInstancia();

Qualquer objeto da aplicação que chame getInstancia() recebe a mesma instância — o que evita, por exemplo, ter que passá-la como parâmetro por diversas camadas do sistema só para que um ponto distante do código possa acessá-la.

Definição: Por que usar Singleton em vez de só métodos estáticos

Como o Singleton é um objeto (não um conjunto de métodos estáticos), a instância única pode ser especializada por herança e encapsulada por um Proxy — o que métodos estáticos, por si só, não permitem.

Definição: O lado negro do Singleton

O Singleton deve ser usado com cuidado, só quando realmente faz sentido ter apenas uma instância da classe — não "porque pode ser útil em qualquer parte do sistema". Usado sem critério, ele acaba funcionando como uma variável global disfarçada de padrão, o que reduz a flexibilidade da modelagem e prejudica bastante a testabilidade: como o acesso é estático, não é possível substituir a instância por um Mock Object em testes automatizados. Inversão de controle e injeção de dependências — assunto de uma seção adiante — ajudam bastante a mitigar esse problema, ao deixar que os próprios métodos estáticos deleguem a lógica de execução para instâncias comuns.

Encapsulando lógica complexa de criação com Builder

Quando a lógica de criação de um objeto é complexa — precisa validar parâmetros, buscar informações em arquivos, ou combinar várias sub-configurações — mantê-la dentro da própria classe (via construtores ou métodos estáticos) deixa a classe grande e confusa.

Outro sintoma clássico do mesmo problema: uma classe com muitos atributos, a maioria opcional, cujo construtor acaba exigindo todos eles de uma vez (o chamado telescoping constructor — um construtor tão grande que vira um "telescópio" de parâmetros). Isso não só torna a criação trabalhosa, como polui os testes da classe com uma quantidade de dados "lixo" (null, "", 0) só para satisfazer parâmetros irrelevantes para o cenário sendo testado — dificultando enxergar, à primeira vista, o que aquele teste realmente está validando.

// construtor "telescópio": todo teste de Carro precisa passar todos os parâmetros,
// mesmo quando só placa e ano importam para aquele teste específico
public class Carro {
    public Carro(String modelo, String fabricante, int anoFabricacao, String placa,
                  String cor, int kmRodados, int anoModelo,
                  long precoMinimo, long precoAnunciado) { /* ... */ }
}

Definição: Builder

Padrão que extrai a lógica de criação de um objeto complexo para uma classe própria, responsável por todo o processo de construção. A partir de um mesmo processo de criação, é possível obter diferentes representações do produto final — o código cliente guia a criação por meio do Builder, sem conhecer a implementação concreta do objeto que está sendo criado.

classDiagram
    class Cliente
    class Builder {
        <<interface>>
        +build()
    }
    class BuilderConcreto {
        +build()
    }
    class Produto {
        <<interface>>
    }
    class ProdutoConcreto
    Cliente ..> Builder
    Builder <|.. BuilderConcreto
    BuilderConcreto ..> ProdutoConcreto : cria
    Produto <|.. ProdutoConcreto

Um exemplo real e conhecido é o AnnotationConfiguration do Hibernate, um Builder para a criação da SessionFactory (a classe que gerencia o acesso ao banco de dados):

SessionFactory sessionFactory = new AnnotationConfiguration()
    .addPackage("com.lojavirtual")
    .addAnnotatedClass(CarrinhoCompras.class)
    .addAnnotatedClass(Cliente.class)
    .addAnnotatedClass(Produtos.class)
    .addResource("orm.xml")
    .configure()
    .buildSessionFactory();

Cada método de configuração devolve a própria instância do Builder (exceto o último, que devolve o produto final já pronto) — esse encadeamento é o que se chama de interface fluente.

Definição: Interface fluente

Termo cunhado por Martin Fowler e Erick Evans para um estilo de API em que os métodos devolvem this, permitindo encadear chamadas de forma que o código se leia quase como uma frase em linguagem natural — uma prática também chamada de DSL (Domain-Specific Language) interna. Só faz sentido quando o processo de criação realmente é complexo o bastante para justificar um Builder; em classes de domínio simples, o padrão Java Bean tradicional (get/set) continua sendo o mais comum, inclusive por exigência de frameworks.

public class BuilderGerador {
    private GeradorArquivo instancia;

    public BuilderGerador gerandoEmXML() {
        instancia = new GeradorXML();
        return this;
    }

    public BuilderGerador comCriptografia() {
        adicionaProcessador(new Criptografador());
        return this;
    }

    public BuilderGerador assincrono() {
        instancia = new ProxyAssincrono(instancia);
        return this;
    }

    public GeradorArquivo construir() {
        return instancia;
    }
}
GeradorArquivo ga = new BuilderGerador()
    .gerandoEmXML().comCriptografia().assincrono().construir();

Repare que o próprio Builder pode envolver o produto num Proxy (assincrono()) sem que o código cliente perceba — o Builder combina livremente com outros padrões de criação e estruturais já vistos neste capítulo (Template Method, Proxy, Composite), o que aumenta a complexidade da criação, mas não deveria ser usado quando essa complexidade não existe de fato.

Definição: Builder x Factory — como decidir

Os dois resolvem o mesmo problema geral (separar a criação de um objeto da lógica de negócio que o utiliza), então a escolha entre eles depende só da complexidade do processo de criação: se o objeto tem poucos atributos ou não precisa de muitas opções, Factory Method (ou até Simple Factory) já resolve, de forma mais simples. Se existem muitos atributos, boa parte deles opcional, ou é preciso oferecer várias combinações possíveis na criação, Builder é a escolha mais adequada. Como os dois têm complexidade de implementação diferente, é comum um projeto começar com uma fábrica simples e só evoluir para Builder conforme o processo de criação realmente cresce.

Relacionando famílias de objetos com Abstract Factory

Outro problema recorrente: quando mais de um objeto relacionado precisa ser criado junto, e existem várias famílias diferentes de implementações que não podem ser misturadas entre si — por exemplo, várias implementações de uma mesma API fornecidas por fornecedores diferentes (a API JDBC é um caso real: misturar um ResultSet do MySQL com um Statement do PostgreSQL, obtidos de conexões diferentes, gera erro).

Definição: Abstract Factory

Padrão que, em vez de fabricar um único objeto, é responsável por criar uma família inteira de objetos relacionados — garantindo que todos vêm da mesma implementação/fornecedor, sem risco de misturar objetos incompatíveis entre si. Cada implementação da fábrica abstrata sabe criar todos os tipos de produto de uma família específica.

classDiagram
    class FabricaAbstrata {
        <<interface>>
        +criarProdutoA()
        +criarProdutoB()
    }
    class FabricaFamiliaX {
        +criarProdutoA()
        +criarProdutoB()
    }
    class FabricaFamiliaY {
        +criarProdutoA()
        +criarProdutoB()
    }
    class ProdutoA {
        <<interface>>
    }
    class ProdutoB {
        <<interface>>
    }
    FabricaAbstrata <|.. FabricaFamiliaX
    FabricaAbstrata <|.. FabricaFamiliaY
    FabricaFamiliaX ..> ProdutoA : cria
    FabricaFamiliaX ..> ProdutoB : cria
    FabricaFamiliaY ..> ProdutoA : cria
    FabricaFamiliaY ..> ProdutoB : cria
public interface ConexaoOperadora {
    FiltroSMS criarFiltro(String expressao);
    EnviadorSMS criarEnviador();
    ObservadorSMS criarObservador();
    ObservadorSMS criarObservadorComFiltro(FiltroSMS f);
    RespondedorAutomatico criarRespostaAutomatica(FiltroSMS f);
}

Cada operadora de telefonia móvel (cada "família") implementa ConexaoOperadora à sua maneira — um FiltroSMS criado pela operadora A nunca é passado, por exemplo, a um ObservadorSMS da operadora B, porque a assinatura dos métodos só aceita os tipos criados pela própria fábrica. Concentrar toda a criação numa única abstração evita esse tipo de mistura acidental, ao mesmo tempo em que permite que o código cliente interaja com qualquer operadora de forma transparente.

Definição: Trade-off do Abstract Factory

Adicionar uma família nova é fácil: basta criar uma nova implementação da fábrica abstrata. Adicionar um tipo de produto novo à família é difícil: exige alterar a abstração da fábrica (um método novo) e, com isso, todas as suas implementações existentes.

Encerrando: criação de objetos

Static Factory Method encapsula a criação de instâncias atrás de um método estático com nome expressivo, capaz de devolver instâncias já existentes ou subtipos; Singleton é o caso especial em que só uma instância deve existir; Builder assume o processo de criação quando ele é complexo demais para caber num construtor ou método estático só; e Abstract Factory garante consistência quando várias instâncias relacionadas, de uma mesma família, precisam ser criadas juntas.

Modularidade

Os padrões de criação vistos até aqui desacoplam a classe cliente das implementações concretas, mas ela continua dependendo diretamente de quem cria essas implementações (um Builder, uma fábrica) — e essa dependência, mesmo indireta, ainda impede que cliente e implementações sejam divididas em módulos totalmente independentes, capazes de evoluir (e ser adicionados ao sistema) sem a recompilação um do outro.

Fábrica dinâmica de objetos

Definição: Reflexão (Reflection)

Capacidade de um programa executar computações a respeito de si mesmo em tempo de execução — obter informações sobre suas próprias classes e instanciá-las a partir de um nome (uma String), em vez de um new fixo no código-fonte. Em Java, a API java.lang.reflect cobre a parte de obter informações e instanciar; funcionalidades de modificação de classes em tempo de execução são mais comuns em linguagens dinâmicas.

Definição: Dynamic Factory

Padrão (descrito em The Dynamic Factory Pattern, PLoP 2008) aplicável quando uma classe precisa criar um objeto de uma abstração conhecida, mas cuja implementação concreta não pode ser determinada em tempo de compilação — só é definida depois, por configuração (um arquivo, um banco, anotações). Usa reflexão para instanciar a classe certa a partir dessa informação (o metadado) sobre qual implementação usar, permitindo que novas implementações sejam adicionadas ao classpath da aplicação sem exigir nenhuma modificação de código.

Definição: Metadado (neste contexto)

"Dado sobre o dado" — no contexto de uma classe, seus metadados são as informações a respeito dela mesma: seus atributos, métodos, interfaces, superclasse. No Dynamic Factory, o metadado relevante é justamente qual classe concreta deve ser instanciada para uma abstração — essa informação pode vir de um arquivo de configuração, de um banco de dados, ou de anotações.

classDiagram
    class Cliente
    class FabricaDinamica {
        +criarInstancia()
    }
    class LeitorMetadados {
        +recuperaImplementacao()
    }
    class Produto {
        <<interface>>
    }
    Cliente ..> FabricaDinamica
    FabricaDinamica ..> LeitorMetadados
    FabricaDinamica ..> Produto : cria
public class FabricaDinamica {
    private Properties props;

    public FabricaDinamica(String arquivo) throws IOException {
        props = new Properties();
        props.load(new FileInputStream(arquivo));
    }

    public <E> E criaImplementacao(Class<E> interfac) {
        String nomeClasse = props.getProperty(interfac.getName());
        try {
            Class clazz = Class.forName(nomeClasse);
            if (interfac.isAssignableFrom(clazz)) {
                return (E) clazz.newInstance();
            } else {
                throw new IllegalArgumentException("Classe configurada não implementa a interface");
            }
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Classe configurada não existe", e);
        }
    }
}

A classe que implementa cada interface é lida de um arquivo de propriedades (interface= NomeDaClasseConcreta), obtida via Class.forName() e instanciada via newInstance() — o cliente nunca referencia a implementação diretamente, nem no código-fonte nem por import. Um Dynamic Factory costuma servir de base para os padrões a seguir: ele resolve o carregamento dinâmico da classe, mas raramente é usado sozinho como estratégia de criação geral — normalmente é combinado com um Builder, Static Factory Method ou Proxy.

Injeção de dependências (Dependency Injection)

Quando um objeto precisa de outro para cumprir sua responsabilidade, é natural que ele mesmo crie ou busque essa dependência — mas isso acopla o objeto à sua criação, prejudicando a modularidade. A alternativa é nunca deixar que a própria classe crie suas dependências: elas são criadas fora, e inseridas (injetadas) no objeto no momento ou depois de sua criação.

Definição: Dependency Injection (Injeção de Dependências)

Padrão em que uma classe externa — o "montador" — é responsável por criar as implementações corretas de cada dependência de um objeto e conectá-las a ele, de forma que o objeto nunca precise criar ou buscar suas próprias dependências. Também chamado de Inversão de Controle (o nome mais usado quando o contexto é frameworks, cujas classes chamam código da aplicação, invertendo o controle do fluxo de execução) — "Dependency Injection" acabou prevalecendo como nome do padrão em si.

classDiagram
    class Montador {
        +montar()
    }
    class Dependencia {
        <<interface>>
    }
    class ImplementacaoDependencia
    class Produto
    Montador ..> ImplementacaoDependencia : cria
    Montador ..> Produto : cria e injeta
    Dependencia <|.. ImplementacaoDependencia
    Produto o--> Dependencia

Definição: Formas de injetar dependências

  • Por método setter — a mais comum; permite reconfigurar a dependência a qualquer momento, mas não garante que ela exista antes do uso.
  • Pelo construtor — a dependência é obrigatória desde a criação do objeto; resolve as mesmas limitações de expressividade de construtores já vistas neste capítulo, e não funciona bem para dependências bidirecionais (duas classes que dependem uma da outra não podem ambas receber a outra pronta no construtor).
  • Por interface — a classe implementa uma interface que declara os métodos pelos quais deve receber cada dependência; mais burocrática, mas deixa explícito, para o montador, quais objetos uma classe precisa receber.
public class AcessoDados {
    private Connection connection;
    public AcessoDados(Connection c) {
        connection = c;
    }
}

Definição: Efeitos colaterais da Dependency Injection

Uma dependência que não é configurada (esquecida na montagem) só falha em tempo de execução — tipicamente com um NullPointerException na primeira chamada a um método nela — o que exige testes de integração, além dos testes de unidade, para garantir que a aplicação está montada corretamente (não só que cada classe funciona isoladamente). A montagem dinâmica também dificulta enxergar o contexto global de como os objetos interagem: ao encontrar um erro, não dá para saber, só olhando a classe, qual implementação concreta foi de fato invocada ali — é preciso checar a configuração do montador.

Frameworks como Spring e o CDI do Java EE implementam o papel de montador, criando instâncias a partir de configurações em XML ou anotações — raramente é necessário escrever essa classe montadora à mão.

<bean id="compactador" class="br.com.casadocodigo.Compactador" />
<bean id="geradorArquivo" class="br.com.casadocodigo.GeradorXML">
    <property name="processador" ref="compactador" />
</bean>

Definição: Vantagem da Dependency Injection na testabilidade

Como a dependência é sempre recebida de fora (nunca criada internamente pela própria classe), um Mock Object pode ser injetado no lugar da implementação real durante um teste, isolando a classe testada — o mesmo problema de testabilidade que o Singleton tem, resolvido aqui de fábrica.

Service Locator

Definição: Service Locator

Alternativa à Dependency Injection para obter modularidade: em vez de receber suas dependências prontas de um montador externo, cada classe busca ativamente a implementação de que precisa, delegando essa busca a uma classe especializada — o "localizador". A classe principal declara o tipo de serviço que precisa (uma abstração) e usa o Service Locator para encontrar quem o presta.

classDiagram
    class Cliente
    class LocalizadorServicos {
        +encontraServico()
    }
    class Servico {
        <<interface>>
        +executarServico()
    }
    class ServicoConcreto {
        +executarServico()
    }
    Cliente ..> LocalizadorServicos
    LocalizadorServicos ..> ServicoConcreto : encontra e cria
    Servico <|.. ServicoConcreto

Definição: Service Locator como Core J2EE Pattern

Documentado originalmente como um Core J2EE Pattern, usado sobretudo para localização remota de enterprise beans — conectando-se a um repositório JNDI e fazendo cache das referências para evitar buscas repetidas. Com a evolução da plataforma (que perdeu até o "2" do nome, virando simplesmente Java EE), esse uso específico ficou obsoleto, já que a própria plataforma passou a fazer injeção de dependências nativamente — o que levou muita gente a achar o padrão em si ultrapassado. Fora desse contexto específico, porém, ele continua útil sempre que se quer modularidade sem um montador central.

A própria JDK oferece um Service Locator pronto: a classe ServiceLoader carrega dinamicamente as implementações de uma interface a partir dos arquivos .jar presentes no classpath — cada JAR registra suas implementações num arquivo de texto dentro de META-INF/services/<nome-completo-da-interface>, listando uma classe por linha.

ServiceLoader<Abstracao> sl = ServiceLoader.load(Abstracao.class);
Iterator<Abstracao> i = sl.iterator();
while (i.hasNext()) {
    System.out.println(i.next().getClass().getName());
}

Bastar incluir (ou remover) um .jar do classpath para que suas implementações passem a ser (ou deixem de ser) retornadas pelo ServiceLoader — sem nenhuma recompilação do código já existente.

Definição: Um Service Locator não deveria vazar como dependência externa

Uma boa prática ao usar esse padrão é mantê-lo encapsulado dentro da classe que precisa do serviço (usado internamente, dentro de um construtor, por exemplo), em vez de expô-lo como uma dependência visível na API pública da classe — assim, quem usa a classe não precisa nem saber que um Service Locator está envolvido.

Service Locator x Dependency Injection

Definição: A diferença central — quem monta a aplicação

Com Dependency Injection, a responsabilidade de montar a configuração de objetos fica centralizada numa classe (o montador), que precisa conhecer todas as dependências de todas as classes envolvidas. Com Service Locator, cada classe é responsável por buscar suas próprias dependências a partir de uma classe localizadora — a responsabilidade fica distribuída, e novas classes podem definir novos pontos de extensão sem que exista um ponto central que precise conhecer cada nova dependência.

Definição: Quando preferir cada um

Dependency Injection tende a ser melhor quando o contexto é uma aplicação em que se deseja modularidade para manutenção e evolução, mas sem a necessidade de adicionar novas classes dinamicamente em tempo de execução. Service Locator se encaixa melhor em arquiteturas baseadas em plugins, em que não existe, na aplicação principal, um ponto central com consciência de todas as dependências possíveis. Quanto à testabilidade, Dependency Injection leva vantagem — a injeção de um Mock Object é direta e explícita nos construtores/interfaces da classe, enquanto substituir o que um Service Locator retorna exige configurar o próprio localizador para o teste.

Adicionando operações

Os padrões vistos até agora ou adicionam uma lógica transversal a um método já existente (Proxy/Decorator, que agem antes/depois da chamada original, sem alterar o que ela representa), ou permitem trocar/combinar implementações de um comportamento já modelado como método (Strategy, Bridge, Composite, Chain of Responsibility). Nenhum deles resolve um problema diferente: incorporar uma operação nova, ainda não prevista pela classe, sem precisar modificá-la a cada vez que uma operação é criada.

Command

Um cenário comum: uma interface gráfica em que um botão, item de menu ou atalho de teclado precisa acionar uma operação, sem que o componente que aciona precise conhecer os detalhes de como essa operação é executada.

Definição: Command

Padrão que representa uma operação como uma classe, em vez de apenas um método. A abstração (interface ou superclasse) define um método de execução (executar()); os comandos concretos implementam essa abstração e guardam, como atributos, todos os dados necessários para a própria execução. O resultado é um comando que representa uma operação do sistema independente do contexto que o originou — podendo ser passado como parâmetro, armazenado, ou executado por outra classe, em outro momento.

public interface ComandoCarrinho {
    Object executar();
}

public class TamanhoParaDownload implements ComandoCarrinho, CienteDosProdutos {
    private List<Produto> produtos;

    public void setListaProdutos(List<Produto> produtos) {
        this.produtos = produtos;
    }

    public Object executar() {
        double tamanho = 0;
        for (Produto p : produtos) {
            if (p.isDigital()) tamanho += p.getTamanhoDownload();
        }
        return tamanho;
    }
}
public class CarrinhoCompras {
    private Map<String, ComandoCarrinho> comandos;
    private List<Produto> produtos;
    private Usuario usuario;

    public Object executaComando(String nomeComando) throws ComandoNaoEncontradoException {
        ComandoCarrinho c = comandos.get(nomeComando);
        if (c == null) throw new ComandoNaoEncontradoException();
        if (c instanceof CienteDosProdutos) {
            ((CienteDosProdutos) c).setListaProdutos(produtos);
        }
        if (c instanceof CienteDoUsuario) {
            ((CienteDoUsuario) c).setUsuario(usuario);
        }
        return c.executar();
    }
}

As interfaces CienteDosProdutos/CienteDoUsuario sinalizam de quais informações cada comando precisa — o próprio CarrinhoCompras decide, por instanceof, o que injetar em cada um antes da execução, sem que os comandos precisem conhecer a classe do carrinho.

Definição: Consequências do Command

Positivo: novas operações se tornam fáceis de incorporar (bastando criar mais uma classe), e como cada comando é uma representação independente da operação, ele pode ser armazenado num histórico, enviado pela rede, ou ter sua execução adiada. Negativo: o número de classes do sistema cresce (uma por operação), e o encapsulamento da classe relacionada à execução pode ficar prejudicado, já que o comando às vezes precisa acessar informações dela que fariam mais sentido internas. Usar Dependency Injection para injetar essas informações no comando (por interfaces, como no exemplo, ou por Dynamic Factory/Service Locator) ajuda a mitigar esse segundo problema.

Cenários de aplicação do Command

  • Execução remota — como um Command é um objeto independente de contexto, ele pode ser serializado e enviado para execução numa máquina remota (um Session Bean, por exemplo), que expõe só um método genérico de execução em vez de um método remoto por operação — reduzindo a superfície da interface de serviço remoto a um único ponto de entrada. A contrapartida é de segurança: como qualquer comando pode ser executado remotamente, é importante restringir quais comandos são aceitos.
  • Transações e log de auditoria — como cada operação é um objeto, ela pode ser armazenada num histórico (para reexecução em caso de queda do sistema, ou para auditoria de quem executou o quê). O framework Prevayler (implementação do padrão Prevalent System) usa exatamente essa ideia: mantém os objetos persistidos em memória, grava periodicamente um snapshot em disco, e nos intervalos entre snapshots registra cada Command executado — para recuperar o estado após uma queda, basta recarregar o último snapshot e reexecutar os comandos registrados depois dele.
  • Fazer e desfazer (undo/redo) — acrescentando um método desfazer() à abstração do comando (além de executar()), um executor pode manter duas pilhas — feitas e desfeitas — empilhando cada comando executado na primeira; ao desfazer, desempilha da primeira, chama desfazer(), e empilha na segunda; ao refazer, o processo se inverte. Esvaziar a pilha de desfeitas sempre que um novo comando é executado evita refazer um comando que não é mais o próximo da sequência histórica.

Definição: Combine padrões, mas para problemas diferentes

Combinar padrões (como o Command combinado com Template Method, Proxy, Dynamic Factory ou Chain of Responsibility, todos vistos ao longo deste livro) é um recurso poderoso, mas deve ser feito com cuidado — usar vários padrões juntos sem necessidade só torna a solução mais complexa, sem trazer benefício real. Um exercício útil antes de somar mais um padrão a uma solução: perguntar explicitamente qual problema cada padrão já presente está resolvendo. Se a resposta não vier facilmente para algum deles, considere removê-lo.

Double Dispatch: delegando a decisão para o parâmetro

Uma limitação sutil de orientação a objetos: quando uma nova variação de tipo precisa de um comportamento diferente por tipo de parâmetro (não pelo tipo do objeto que recebe a chamada), sobrecarga de método (mesmo nome, parâmetros diferentes) obriga a alterar a classe que recebe a chamada toda vez que um tipo de parâmetro novo aparece.

Definição: Double Dispatch

Técnica em que um método recebe um parâmetro e, em vez de tratá-lo diretamente, devolve a chamada para um método do próprio parâmetro, passando this (a própria classe que recebeu a chamada original) como argumento. A delegação acontece duas vezes: a primeira classe delega para o parâmetro, que delega de volta — "despacho duplo". Isso permite que cada tipo do parâmetro decida sua própria lógica de interação com a classe original, sem exigir que ela conheça, de antemão, todos os tipos possíveis de parâmetro.

public class CarrinhoCompras {
    public void adicionarProduto(Produto p) {
        produtos.add(p);
        p.adicionaPropriedades(this); // devolve a chamada para o parâmetro
    }
}

public class ProdutoDigital extends Produto {
    @Override
    public void adicionaPropriedades(CarrinhoCompras c) {
        c.adicionaPropriedade("PRECO", getPreco());
        c.adicionaPropriedade("DOWNLOAD", getTamanho());
    }
}

Sem essa técnica, CarrinhoCompras precisaria de um if (produto instanceof ...) (ou um método de sobrecarga) para cada tipo de produto; com ela, cada subclasse de Produto decide sozinha quais propriedades adiciona ao carrinho — um tipo de produto novo não exige nenhuma alteração em CarrinhoCompras.

classDiagram
    class Despachante {
        +aceitar(in Elemento)
    }
    class Elemento {
        <<interface>>
        +operacaoRetorno(in Despachante)
    }
    class ElementoConcretoA {
        +operacaoRetorno(in Despachante)
    }
    class ElementoConcretoB {
        +operacaoRetorno(in Despachante)
    }
    Despachante ..> Elemento
    Elemento <|.. ElementoConcretoA
    Elemento <|.. ElementoConcretoB
    ElementoConcretoA ..> Despachante
    ElementoConcretoB ..> Despachante

Definição: Dependência cíclica é característica, não defeito

A estrutura do Double Dispatch tem uma dependência cíclica proposital entre Despachante e Elemento: o primeiro conhece a abstração do segundo, e o segundo recebe o primeiro como parâmetro do método que implementa. Apesar de cíclica, a dependência real é só com as abstrações de cada lado — nenhum dos dois depende de nenhuma implementação concreta do outro, então novas implementações de qualquer um dos dois lados continuam fáceis de incorporar.

Padrão Visitor

Um problema mais amplo que o do Double Dispatch: uma família de elementos que precisa ser processada de formas completamente diferentes dependendo de quem está processando — por exemplo, os elementos de um relatório (título, parágrafo, tabela) sendo gerados em HTML, PDF, planilha ou XML. Como cada formato de saída usa uma API diferente para se construir, e cada relatório tem elementos próprios, tentar reutilizar código de geração entre relatórios ou entre formatos leva a uma explosão de métodos (um por combinação relatório × formato).

Definição: Visitor

Padrão baseado em Double Dispatch para adicionar operações a uma hierarquia inteira de classes ("elementos") sem alterá-las a cada operação nova. Existem duas hierarquias: elementos (cada um implementa um método aceitar(visitante)) e visitantes (cada um implementa um método por tipo de elemento que sabe visitar). Quando um elemento recebe um visitante, ele devolve a chamada — visitante.visitarX (this) — deixando que a implementação do visitante decida o que fazer com aquele elemento específico. Uma nova operação sobre toda a hierarquia de elementos vira, então, uma nova implementação de visitante — nenhum elemento precisa mudar.

Uma forma de fixar a ideia: imagine duas pintoras de estilos diferentes (uma clássica, uma cubista) visitando os mesmos dois locais (uma favela, uma casa de campo). O pedido feito a cada uma é o mesmo ("pinte este local"), mas o resultado depende tanto do local visitado quanto de quem o visita — a mesma dualidade do Visitor, em que o comportamento final depende da combinação elemento × visitante.

classDiagram
    class Visitante {
        <<interface>>
        +visitarElementoA()
        +visitarElementoB()
    }
    class VisitanteX {
        +visitarElementoA()
        +visitarElementoB()
    }
    class Elemento {
        <<interface>>
        +aceitar(in Visitante)
    }
    class ElementoA {
        +aceitar(in Visitante)
    }
    class ElementoB {
        +aceitar(in Visitante)
    }
    Visitante <|.. VisitanteX
    Elemento <|.. ElementoA
    Elemento <|.. ElementoB
    ElementoA ..> Visitante
    ElementoB ..> Visitante
public interface FormatoVisitante {
    void visitarTitulo(String t);
    void visitarParagrafo(String p);
    void visitarTabela();
    void visitarTabelaCabecalho(String... ct);
    void visitarTabelaLinha(Object... o);
    void visitarTabelaFim();
    Object getResultado();
}

public interface Relatorio {
    Object gerarRelatorio(FormatoVisitante fv);
}

public class ComprasCliente implements Relatorio {
    private Cliente c;
    private List<Item> items;

    public Object gerarRelatorio(FormatoVisitante fv) {
        fv.visitarTitulo("Compras de " + c.getNome());
        fv.visitarTabela();
        fv.visitarTabelaCabecalho("Produto", "Data", "Valor");
        for (Item i : items) {
            fv.visitarTabelaLinha(i.getProduto(), i.getDataCompra(), i.getValor());
        }
        fv.visitarTabelaFim();
        return fv.getResultado();
    }
}
public class VisitanteHTML implements FormatoVisitante {
    private StringBuilder sb = new StringBuilder();

    public void visitarTitulo(String t) { sb.append("<h1>" + t + "</h1>"); }
    public void visitarParagrafo(String p) { sb.append("<p>" + p + "</p>"); }
    // ... demais métodos, cada um adicionando sua própria marcação HTML

    public Object getResultado() { return sb.toString(); }
}
Relatorio r = new ComprasCliente();
FormatoVisitante fv = new VisitanteHTML();
String resultado = (String) r.gerarRelatorio(fv);

ComprasCliente nunca menciona HTML, PDF ou qualquer outro formato — ela só invoca os métodos genéricos de FormatoVisitante, na ordem que faz sentido para sua própria estrutura. Um formato novo é só uma implementação nova de FormatoVisitante; um relatório novo é só uma implementação nova de Relatorio — nenhum dos dois lados obriga o outro a mudar.

Definição: Visitor x Double Dispatch — onde está a diferença

O Visitor usa Double Dispatch como mecanismo interno, mas vai além: no Double Dispatch puro, a variação está em qual implementação do parâmetro é passada; no Visitor, o próprio "visitante" pode ter várias implementações e definir métodos diferentes por tipo de elemento aceito, permitindo bem mais flexibilidade sobre quais operações fazem sentido por elemento e qual sequência de chamadas cada implementação de elemento realiza.

Definição: Visitor com Composite

Um elemento visitado pode, ele mesmo, ser composto por outros elementos — nesse caso, o elemento composto repassa o visitante recebido para cada elemento que o compõe, delegando parte da execução a eles. É assim que um relatório poderia ser composto por sub-relatórios reutilizáveis, cada um sabendo se apresentar ao mesmo visitante.

Definição: A dificuldade real do Visitor

O Visitor é conhecido por ser um dos padrões GoF mais difíceis de compreender e aplicar corretamente — em parte porque a maioria dos exemplos didáticos ilustra só a estrutura, sem a motivação real por trás dela. A manutenção também tem um ponto fraco específico: adicionar um elemento novo à hierarquia exige um método novo na interface do visitante — e, com isso, uma alteração em todas as implementações de visitante já existentes. Nas situações em que ele realmente se aplica (quando as operações mudam com mais frequência que os elementos), porém, os ganhos de reúso e manutenção compensam bastante essa dificuldade inicial.

Encerrando: operações como cidadãos de primeira classe

Os três padrões deste capítulo compartilham a ideia de tratar uma operação como algo que pode ser passado, armazenado e trocado, tanto quanto qualquer outro objeto: Command representa uma operação inteira como uma classe; Double Dispatch delega a decisão de qual comportamento executar para o próprio parâmetro recebido; Visitor generaliza essa delegação para uma família inteira de novas operações sobre uma hierarquia de classes já existente, sem precisar alterá-la a cada operação nova.

Gerenciando muitos objetos

Mesmo um software bem modelado, com os padrões já vistos aplicados corretamente, pode enfrentar dois problemas de escala à medida que cresce: (1) a quantidade de classes que um cliente precisa conhecer e acoplar-se para realizar uma tarefa fica grande demais, e (2) a quantidade de instâncias de uma mesma classe, em memória, cresce mais do que o necessário quando muitas delas são, na prática, idênticas entre si.

Facade

Definição: Facade

Padrão que cria uma classe intermediária — a fachada — para servir de ponto único de acesso a um conjunto de classes (um subsistema), escondendo do cliente sua complexidade interna e suas implementações específicas. O cliente passa a interagir só com a fachada; toda a coordenação entre as classes do subsistema fica encapsulada dentro dela.

classDiagram
    class Cliente
    class Fachada {
        +metodoA()
        +metodoB()
    }
    class ClasseSubsistemaA
    class ClasseSubsistemaB
    class ClasseSubsistemaC
    Cliente ..> Fachada
    Fachada ..> ClasseSubsistemaA
    Fachada ..> ClasseSubsistemaB
    Fachada ..> ClasseSubsistemaC
public class GeradorArquivoFacade {
    public void gerarXMLCompactado(String nome, Map<String, Object> propriedades) {
        GeradorArquivo g = new GeradorXML();
        g.setProcessador(new Compactador());
        g.gerarArquivo(nome, propriedades);
    }

    public void gerarPropriedadesCriptografado(String nome, Map<String, Object> propriedades) {
        GeradorArquivo g = new GeradorPropriedades();
        g.setProcessador(new Criptografador());
        g.gerarArquivo(nome, propriedades);
    }
    // ... só as combinações realmente usadas pela aplicação
}

A fachada não substitui a API original (GeradorArquivo e seus pós-processadores continuam existindo e podendo ser usados diretamente) — ela só oferece um caminho mais simples para os casos de uso mais comuns, sem exigir que o cliente conheça Bridge, Template Method ou qualquer outro padrão por trás da implementação.

Definição: Refatorando para um Facade

A necessidade de uma fachada raramente aparece logo no início de um projeto — ela surge conforme as interações entre classes do subsistema vão se espalhando pelo código cliente. A refatoração é simples e incremental: (1) identificar, no código cliente, o trecho que interage com várias classes do subsistema e extraí-lo para um método; (2) mover esse método para uma classe fachada. Repetir esse processo, ponto por ponto, migra gradualmente o acesso ao subsistema para a fachada, sem quebrar o que já funciona.

Definição: Facade x Adapter — não confundir

Apesar de ambos encapsularem outra(s) classe(s), o objetivo é diferente. Adapter adapta uma classe existente para uma interface já esperada pelo cliente (resolve incompatibilidade), tipicamente encapsulando uma única classe. Facade define uma interface nova, pensada para simplificar o uso de várias classes ao mesmo tempo — não existe uma interface prévia que ela precise respeitar.

Definição: Anti-Corruption Layer — um Facade para código legado

Padrão descrito por Eric Evans em Domain-Driven Design, aplicável quando uma aplicação nova precisa acessar um sistema legado sem deixar o modelo antigo "contaminar" o novo design. Uma camada de tradução (normalmente implementada como um Facade, às vezes com um Adapter por trás para traduzir as classes de dados de um modelo para o outro) fica entre as duas aplicações, isolando a existência do sistema legado do restante do código novo.

Definição: Facade como base de componentes plugáveis

Combinada com os padrões de criação já vistos (Dynamic Factory, Dependency Injection, Service Locator), a fachada de um componente pode ser definida como uma interface, com múltiplas implementações instanciadas dinamicamente — a base de uma arquitetura de componentes plugáveis, em que cada componente pode ser substituído ou adicionado sem alterar quem o consome.

Mediator

O Facade resolve o acesso a um subsistema já bem definido, mas nem toda interação entre objetos acontece numa única direção — objetos às vezes precisam se comunicar de forma bidirecional, criando interdependências difíceis de entender e manter (um caso comum em formulários de interface gráfica: o valor escolhido num campo habilita, desabilita ou valida outros campos, numa relação muitos-para-muitos entre eles).

Definição: Mediator

Padrão que cria uma classe — o mediador — para concentrar a lógica de interação entre vários objetos, no lugar de eles se comunicarem diretamente uns com os outros. Em vez de enviar e receber requisições de vários outros objetos, cada objeto passa a interagir só com o mediador, que recebe as requisições e as encaminha para quem deve recebê-las.

Definição: Duas analogias úteis para o Mediator

  • Tabela associativa de banco de dados: ao modelar um relacionamento muitos-para-muitos entre duas tabelas, a solução padrão é uma tabela de associação que guarda referências para os dois lados, em vez de cada linha de uma tabela referenciar diretamente várias linhas da outra. O Mediator faz o mesmo em memória: centraliza uma relação muitos-para-muitos entre objetos numa classe própria.
  • Central telefônica: ao ligar para uma empresa, quem liga não precisa saber o ramal exato de cada setor — só disca um número único, e a central redireciona a chamada para quem deve atendê-la. Da mesma forma, quem aciona o mediador não precisa conhecer, de antemão, qual objeto específico vai tratar aquela chamada.
public class GrupoObservacao implements Observador {
    private List<Observador> obs = new ArrayList<>();

    public void mudancaQuantidade(String acao, Integer qtd) {
        for (Observador o : obs) o.mudancaQuantidade(acao, qtd);
    }

    public void addObservador(Observador o) { obs.add(o); }

    public void addCarteira(CarteiraAcoes ca) { ca.addObservador(this); }
}

GrupoObservacao media a relação entre várias CarteiraAcoes e vários observadores: ele mesmo se registra como observador em cada carteira adicionada, e repassa cada notificação recebida para todos os observadores que registrou — sem que carteiras e observadores precisem se conhecer diretamente, mesmo quando o número de cada lado cresce.

classDiagram
    class RecebedorChamadas {
        <<interface>>
        +receber()
    }
    class Mediador {
        +recebeChamada()
        +realizaChamadas()
    }
    class RealizadorChamadas {
        +chamarMediador()
    }
    class RecebedorConcreto {
        +receber()
    }
    RecebedorChamadas <|.. RecebedorConcreto
    RecebedorChamadas <|.. Mediador
    Mediador ..> RecebedorChamadas
    RealizadorChamadas <|.. Mediador

Definição: Quando vale a pena introduzir um Mediator

Faz sentido quando as relações entre objetos ficam complexas o bastante para justificar concentrar essa responsabilidade numa única classe — tipicamente por causa de uma grande quantidade de objetos interligados, ou de regras específicas sobre quando um determinado objeto deve (ou não) receber uma chamada. A refatoração parte de extrair, aos poucos, a lógica de interação espalhada nas classes envolvidas para o mediador, incorporando novas regras nele sem eliminar as antigas.

Um jeito interessante de implementar essa ideia sem escrever um mediador do zero é usar eventos — o Spring, por exemplo, permite lançar objetos que estendem ApplicationEvent, tratados por qualquer classe registrada como ApplicationListener daquele tipo de evento. Quem gera o evento não conhece quem vai tratá-lo, e vice-versa — é o próprio framework que age como mediador entre eles, entregando o evento a todos os interessados.

Definição: Facade x Mediator — direção do fluxo

Os dois centralizam responsabilidade numa classe intermediária, mas por motivos diferentes. Facade simplifica o acesso a um subsistema já coeso, numa direção (cliente → subsistema). Mediator organiza uma comunicação bidirecional entre objetos que, de outra forma, se acoplariam diretamente uns aos outros — os próprios objetos mediados podem, eles mesmos, ser os autores das chamadas que o mediador coordena.

Flyweight

Um problema diferente dos dois anteriores: não é a quantidade de classes, mas a quantidade de instâncias de uma mesma classe que se torna um problema de desempenho — muitas vezes descoberto só com uma ferramenta de profiling (software que monitora, em tempo de execução, quanto tempo e quanta memória cada classe consome), ao perceber um número enorme de instâncias praticamente idênticas de uma classe pequena.

Definição: Flyweight (peso-mosca)

Padrão que reaproveita a mesma instância para representar objetos semelhantes, em vez de criar uma instância nova a cada necessidade — uma fábrica dedicada (FabricaFlyweight) devolve, a partir de uma chave, sempre a mesma instância já existente para aquela chave. Só se aplica quando os objetos representados são, de fato, imutáveis: como a mesma instância passa a ser compartilhada por múltiplos contextos, qualquer informação que dependa do contexto de uso não pode ficar armazenada no próprio objeto — precisa ser passada por parâmetro a cada chamada que precisar dela.

classDiagram
    class Cliente
    class FabricaFlyweight {
        +recuperaFlyweight(in chave)
    }
    class Flyweight {
        <<interface>>
        +executarNoEstadoExterno()
    }
    Cliente ..> FabricaFlyweight
    FabricaFlyweight ..> Flyweight
    Cliente ..> Flyweight
public class FabricaStatusItem {
    private static FabricaStatusItem instance = new FabricaStatusItem();
    private Map<String, StatusItem> mapa;

    public static FabricaStatusItem getInstance() { return instance; }

    private FabricaStatusItem() {
        mapa = new HashMap<>();
        mapa.put("PAGO", new StatusItem("PAGO", true, true));
        mapa.put("ENVIADO", new StatusItem("ENVIADO", false, true));
        // ... um StatusItem por status possível, criado uma única vez
    }

    public StatusItem get(String nome) {
        if (!mapa.containsKey(nome))
            throw new RuntimeException("Status inexistente: " + nome);
        return mapa.get(nome);
    }
}

A fábrica combina Flyweight com Singleton — só deve existir uma instância dela, para garantir que todo StatusItem de um mesmo nome realmente aponte para o mesmo objeto. Isso permite, inclusive, comparar duas instâncias com == em vez de equals(), com a garantia de que duas instâncias iguais são sempre, de fato, a mesma instância.

Definição: Cuidados para garantir a imutabilidade do Flyweight

Como a mesma instância é compartilhada por todo o sistema, qualquer brecha para modificá-la afeta, silenciosamente, todos os lugares que a utilizam — um bug muito difícil de rastrear. Algumas diretrizes práticas: não expor métodos setter; declarar todos os atributos private final; impedir a criação de subclasses (final na classe, ou construtor private combinado com um Static Factory Method); nunca devolver diretamente um atributo que seja, ele mesmo, mutável (devolver uma cópia); e, ao receber um objeto mutável como parâmetro do construtor, guardar uma cópia dele, não a referência recebida — do contrário, o próprio cliente que criou o objeto ainda consegue modificá-lo por fora depois.

Definição: Flyweight e serialização/persistência

Como a unicidade da instância é garantida pela fábrica em memória, ela se perde ao serializar o objeto (ex.: enviá-lo pela rede) ou persisti-lo via JPA — o mecanismo padrão recria uma instância nova na desserialização/carregamento, quebrando a garantia de que duas referências "iguais" sejam a mesma instância. A correção nos dois casos segue a mesma ideia: em vez de serializar/persistir o objeto Flyweight inteiro, guardar só a chave que o identifica (@Transient em JPA, ou transient em serialização Java, com os métodos writeObject()/readObject() sobrepostos) e, na volta, buscar a instância correta de novo na fábrica a partir dessa chave.

Definição: String é um Flyweight

A classe String do Java é imutável — todo método que "modifica" uma string (substring(), concatenação) na verdade retorna uma nova instância, nunca altera a original. Isso é o que permite ao String pool da JVM compartilhar a mesma cadeia de caracteres entre diversas instâncias de String iguais — uma aplicação direta do Flyweight já embutida na própria linguagem.

Encerrando: gerenciando o crescimento do sistema

Facade reduz o número de classes que um cliente precisa conhecer para realizar uma tarefa, concentrando a coordenação de um subsistema numa única interface (o Anti-Corruption Layer é essa mesma ideia aplicada especificamente para isolar código legado); Mediator resolve o problema seguinte — a comunicação bidirecional entre muitos objetos — concentrando essa lógica numa classe intermediária; Flyweight ataca um problema diferente, de memória: reaproveitar a mesma instância entre objetos semelhantes, desde que eles sejam imutáveis. Os três padrões lidam, cada um à sua forma, com o crescimento do sistema — em número de classes, de interações e de instâncias, respectivamente.

Indo além do básico

Os capítulos anteriores apresentaram padrões individuais, cada um resolvendo um problema específico. Esta seção final reúne alguns tópicos mais amplos sobre como os padrões se encaixam em contextos maiores: frameworks, tipos genéricos, TDD e arquitetura.

Frameworks

Definição: Framework

Uma estrutura de software incompleta por design: sozinho, um framework não faz nada — ele precisa ser completado com classes específicas da aplicação para poder ser executado. Diferente de uma biblioteca (onde a aplicação chama o código pronto e controla o fluxo de execução), num framework é o framework quem controla o fluxo, invocando código da aplicação em pontos específicos — uma inversão de controle arquitetural (o mesmo princípio por trás de Dependency Injection, aqui aplicado à execução como um todo, não só à criação de dependências).

Definição: Frozen spots x hot spots

Um framework é composto por frozen spots — a funcionalidade fixa, que o framework já provê pronta e coordena sozinho — e hot spots — os pontos de extensão, onde a aplicação insere sua própria lógica. Juntos, eles formam a arquitetura do framework; identificar corretamente quais partes de um domínio deveriam ser frozen spots e quais deveriam ser hot spots é o principal desafio de projetar um framework.

classDiagram
    class GeradorArquivo {
        #gerarArquivo()
        #processar()
        #gerarConteudo()
    }
    class GeradorConcreto {
        #gerarConteudo()
    }
    class PosProcessador {
        <<interface>>
        +processar()
    }
    class ProcessadorConcreto {
        +processar()
    }
    class ProcessadorComposto {
        +processar()
    }
    GeradorArquivo <|-- GeradorConcreto
    GeradorArquivo o--> PosProcessador
    PosProcessador <|.. ProcessadorConcreto
    PosProcessador <|.. ProcessadorComposto

No próprio GeradorArquivo usado ao longo deste livro, o algoritmo geral de geração (gerarArquivo()) e a coordenação dos pós-processadores (via Composite) são frozen spots; o formato do arquivo (gerarConteudo(), um hook method) e a lista de pós-processadores aplicados são hot spots.

Definição: Os hooks e padrões já vistos são o que viabiliza os hot spots

Boa parte da "engenharia" por trás de um framework é justamente aplicar os padrões já vistos neste livro para criar seus pontos de extensão: hook methods/hook classes (herança e composição como mecanismos de extensão), reflexão e metadados (anotações, cada vez mais comuns em frameworks modernos para configurar hot spots sem exigir código Java explícito).

Definição: Low Surface-to-Volume Ratio

Princípio (do artigo homônimo de Brian Foote e Joseph Yoder) que recomenda que a interface externa de um framework seja compacta, mesmo quando ele possui muitos hot spots internos — tornando-o mais simples de usar sem que o desenvolvedor precise conhecer toda sua estrutura interna. É comum um Facade ser usado exatamente para atingir esse objetivo.

Definição: Gentle Learning Curve (curva de aprendizado suave)

Outra prática comum para simplificar frameworks com muitos hot spots: configurá-los, por padrão, com implementações prontas e razoáveis para a maioria dos casos (assim, o desenvolvedor iniciante não precisa configurar tudo de uma vez), e disponibilizar um Builder para quem precisar personalizar hot spots específicos mais adiante — permitindo que o aprendizado da estrutura interna do framework aconteça aos poucos, conforme necessário.

Exemplos reais de frameworks Java ilustram a variedade de hot spots possíveis: o MVC (Model-View-Controller) tem, no controller, o hot spot que decide qual lógica executar a cada requisição (Servlets, Struts Actions e JSF Managed Beans implementam esse papel); o agendador de tarefas Quartz define, como hot spot, apenas a lógica de negócio a ser executada em cada agendamento — o próprio agendamento não faz sentido fora do framework; e o Log4j combina frozen spots com hot spots configuráveis inteiramente por arquivo de propriedades, sem exigir nenhum código Java — nesse caso, os próprios "nomes de classe" configurados no arquivo (ConsoleAppender, DailyRollingFileAppender) é que preenchem os hot spots dinamicamente.

Definição: Um hot spot pode conter sua própria biblioteca

Nada impede que um hot spot seja preenchido tanto por implementações prontas, fornecidas como uma pequena biblioteca de classes do próprio framework (ex.: Compactador/Criptografador como pós-processadores prontos do GeradorArquivo), quanto por implementações totalmente novas, escritas pela aplicação que usa o framework — as duas formas de uso coexistem no mesmo ponto de extensão.

Tipos genéricos com os padrões

Definição: Tipos genéricos como reforço de contrato entre padrões

Muitos padrões definem uma colaboração entre duas classes (ex.: observável e observador) através de uma interface. Parametrizar essa interface com tipos genéricos (introduzidos no Java 5) permite que o compilador garanta, em tempo de compilação, que só instâncias compatíveis daquele padrão se relacionem entre si — em vez de descobrir uma incompatibilidade de tipos só em tempo de execução, com um ClassCastException.

public interface Observador<E> {
    void notificar(E evento);
}

public interface Observavel<E> {
    void adicionaObservador(Observador<? super E> o);
}

public class ProcessadorCompra implements Observavel<CompraAcao> {
    public void adicionaObservador(Observador<? super CompraAcao> o) { /* ... */ }
}

public class ObservadorOperacao implements Observador<OperacaoAcao> {
    public void notificar(OperacaoAcao evento) { /* ... */ }
}

Como CompraAcao é uma subclasse de OperacaoAcao, um ObservadorOperacao pode se registrar num ProcessadorCompra (Observador<? super CompraAcao> aceita observadores de CompraAcao ou de qualquer superclasse dela) — mas um observador de VendaAcao (outra subclasse de OperacaoAcao) não compilaria contra ProcessadorCompra, porque os tipos genéricos tornam essa incompatibilidade visível já na assinatura do método, não só em tempo de execução.

Padrões com Test-Driven Development

Definição: TDD (Test-Driven Development)

Técnica de desenvolvimento em que os testes são escritos antes do código de produção, em ciclos curtos que alternam entre criar um teste, implementar a solução mais simples que o satisfaz, e refatorar o código resultante (ver também Qualidade). Como o teste é escrito primeiro, o desenvolvedor fica focado na API externa da classe — como ela será usada — antes de se preocupar com a implementação interna.

Definição: Como TDD favorece boas práticas de design

O próprio fato de o teste ser escrito antes empurra a classe testada para características desejáveis: mais fácil de testar uma classe coesa (com uma responsabilidade bem definida) do que uma que acumula várias; e mais fácil de testar uma classe cujas dependências podem ser substituídas por Mock Objects — o que empurra o design em direção ao desacoplamento (frequentemente via Dependency Injection ou Service Locator).

Definição: Onde os padrões de criação entram no TDD

Já no primeiro teste, o desenvolvedor precisa decidir como a classe sendo testada será instanciada — construtor comum, Static Factory Method, Singleton (se só uma instância fizer sentido), ou, se a criação for complexa o bastante, um Builder (desenvolvido, ele mesmo, também via TDD). TDD não substitui o conhecimento de padrões: os testes funcionam como um mecanismo para expressar o design desejado, mas não indicam sozinhos qual design é o correto para um problema — isso continua dependendo do repertório de padrões (e de bom senso) de quem projeta.

Um exemplo prático: ao testar que um CarrinhoCompras notifica corretamente seus observadores quando um produto é adicionado (um caso de uso do padrão Observer), o teste já pode ser escrito contra um Mock Object que simula um observador, sem que a implementação de CarrinhoCompras precise existir ainda:

@Test
public void testeNotificacao() {
    MockObservador mock = new MockObservador();
    CarrinhoCompras cc = new CarrinhoCompras();
    cc.addObservador(mock);

    Produto p = new Produto("Cabo HDMI", 30.0);
    cc.adicionar(p);

    assertTrue(mock.recebeuNotificacao());
    assertEquals(p, mock.produtoRecebido());
}

public class MockObservador implements ObservadorCarrinho {
    private Produto p;

    public void notificaProduto(Produto p) { this.p = p; }
    public boolean recebeuNotificacao() { return p != null; }
    public Produto produtoRecebido() { return p; }
}

A interface ObservadorCarrinho (e seu método notificaProduto()) nasce a partir do teste, antes mesmo de CarrinhoCompras existir — é o padrão Observer, já conhecido de antemão, que orienta qual contrato faz sentido definir nesse momento.

Definição: A refatoração do TDD é onde padrões costumam surgir

Nem sempre a necessidade de um padrão aparece já na primeira versão de uma classe — muitas vezes ele só se torna evidente conforme a classe acumula responsabilidades ou duplicação de código, sinais identificados durante a fase de refatoração do ciclo de TDD. Ter o repertório de padrões deste livro (e o vocabulário de code smells já visto em Boas Práticas) ajuda a reconhecer esses sinais e escolher a refatoração certa quando eles aparecem.

Padrões aplicados à arquitetura

Definição: Um padrão pode ser reaplicado numa escala maior

Como os padrões descrevem problemas e soluções, não implementações específicas, nada impede aplicar a mesma estrutura de um padrão numa escala arquitetural — tratando subsistemas, serviços ou componentes de infraestrutura inteiros como os "participantes" do padrão, em vez de objetos isolados dentro de um mesmo processo.

Um Observer entre subsistemas remotos, por exemplo, resolve o mesmo problema (notificar interessados sobre uma mudança) — mas exige que a comunicação entre observável e observadores seja adaptada para um protocolo de rede, em vez de uma simples chamada de método.

flowchart LR
    Cliente -->|requisição| Proxy
    Proxy -->|encaminha| Servidor
    Servidor -->|resposta| Proxy
    Proxy -->|resposta| Cliente

Um Proxy aplicado nesse nível funciona de forma semelhante ao Proxy de objetos: fica entre o cliente e o servidor, expondo o mesmo protocolo que o servidor original exporia — permitindo, por exemplo, que o servidor real mude de localização sem que o cliente perceba, ou que validações/autenticação sejam adicionadas nesse ponto intermediário, sem alterar nem cliente nem servidor.

Definição: Novas consequências surgem na escala arquitetural

A estrutura se repete, mas as consequências mudam — na escala de objetos, um Proxy não introduz risco de queda de rede; na escala arquitetural, se a máquina que hospeda o Proxy cair, o cliente perde acesso ao serviço inteiro, mesmo que o servidor original continue funcionando normalmente (um novo ponto único de falha). Em compensação, ganha-se a flexibilidade de mudar a localização do servidor real sem que o cliente precise saber. Usar um padrão às cegas, sem reavaliar suas consequências no novo contexto (desempenho, tolerância a falhas, forma de comunicação), é um erro tão importante de evitar na arquitetura quanto no design de classes.

Este último capítulo não trouxe nenhum padrão novo, mas fechou uma volta importante: frameworks são, em grande parte, uma aplicação sistemática dos hooks e padrões de criação já vistos para construir pontos de extensão bem projetados; tipos genéricos reforçam, em tempo de compilação, contratos que os próprios padrões já definiam informalmente; TDD usa o repertório de padrões para guiar decisões de design que os testes, sozinhos, não resolvem; e a mesma estrutura de um padrão pode ser reaplicada numa escala arquitetural inteira, desde que as consequências específicas dessa escala sejam reavaliadas. O fio condutor de todos os padrões deste livro continua sendo o mesmo do primeiro capítulo: reconhecer o problema por trás de cada estrutura é o que permite usá-la (ou não) com critério.

Considerações finais: combinando padrões

Não existe uma única forma correta de aplicar os padrões vistos até aqui — muitas vezes mais de um padrão resolve o mesmo problema, com consequências diferentes (para flexibilizar os passos de um algoritmo, Template Method usa herança e Strategy usa composição; para combinar objetos de forma transparente, tanto Composite quanto Chain of Responsibility servem, dependendo se o objetivo é combinar resultados ou decidir qual elemento trata a requisição). O importante não é aplicar padrões às cegas, e sim avaliar, entre as alternativas existentes, quais consequências fazem mais sentido para o contexto específico da aplicação — inclusive combinando vários padrões numa mesma solução, como o próprio Dynamic Factory combinado com Dependency Injection ou Service Locator para obter, ao mesmo tempo, modularidade e carregamento dinâmico de implementações.

Uma visão crítica: nem todo padrão é sempre bem-vindo

Conhecer o catálogo não significa que todo padrão deva ser aplicado com a mesma frequência — alguns são pouco usados por resolverem um problema muito específico, e outros dois, apesar de populares, são frequentemente citados como antipadrões em certos contextos.

Definição: Padrões pouco utilizados

Alguns padrões resolvem problemas reais, mas raros o bastante para que a maior parte dos projetos nunca precise deles — Chain of Responsibility é um exemplo: ter múltiplos objetos capazes de responder à mesma solicitação é uma situação legítima, mas pouco comum. Antes de aplicar qualquer padrão (mesmo um dos mais consolidados), vale perguntar se o problema realmente não pode ser resolvido de forma mais simples.

Definição: Facade como antipadrão — quando esconder a bagunça não é a solução

O Facade é útil quando ele simplifica o acesso a um conjunto de componentes já bem organizados. O uso vira problemático quando ele é aplicado só para esconder uma bagunça de design já existente, sem resolvê-la de fato — o código por trás continua confuso, só que agora atrás de uma fachada simples. Colocar uma fachada bonita na frente de um problema não é o mesmo que corrigi-lo; o problema não está no Facade em si, mas em aplicá-lo fora do contexto certo.

Definição: Singleton como antipadrão — o problema da variável global

Já visto como o lado negro do Singleton: o padrão resolve a necessidade real de garantir uma única instância, mas o fornece através de um acesso global — o mesmo problema clássico de uma variável global tradicional. Criar e acessar estado global torna difícil ter certeza do estado da aplicação a qualquer momento (qualquer parte do código pode tê-lo alterado antes), e prejudica testes automatizados. Isso não significa nunca usar Singleton — só que, quando a necessidade de instância única for real, vale considerar alternativas com acesso mais controlado (como injeção de dependência de uma instância única gerenciada por um framework) em vez do acesso estático global tradicional.

Simplicidade de código e design evolucionário

Uma dúvida recorrente é quando exatamente introduzir um padrão: no início do projeto, ou só quando a necessidade aparece?

Definição: Simplicidade de código, segundo Kent Beck

No livro Extreme Programming Explained (1999), Kent Beck propõe quatro características que definem um código simples, em ordem de prioridade: (1) todos os testes passam; (2) sem duplicação; (3) mostra as intenções de quem escreveu (nomes e estrutura comunicam o propósito); (4) o menor número possível de classes e métodos. Aplicar um padrão de projeto tende a ajudar nos itens 2 e 3 (reduz duplicação, deixa intenções mais claras) e, ao mesmo tempo, piorar o item 4 (mais classes) — por isso nenhuma dessas características deve ser otimizada isoladamente, e sim em conjunto.

Definição: Design evolucionário

Ideia (discutida por Martin Fowler no artigo Is Design Dead?) de que o design de uma aplicação não precisa (nem deveria) ser todo planejado com antecedência — pode evoluir a partir das necessidades reais de crescimento do projeto, apoiado por uma boa suíte de testes e pela prática constante de refatoração. Fowler sugere algumas diretrizes práticas para aproveitar melhor o catálogo de padrões dentro dessa filosofia: investir tempo aprendendo sobre os padrões antes de precisar deles; concentrar-se em quando aplicá-los (evitando cedo demais); praticar a implementação da forma mais simples possível, adicionando complexidade só quando necessário; e, se um padrão já aplicado não estiver ajudando, não ter medo de removê-lo — um padrão não é uma decisão permanente e irreversível.