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
ApplicationeInfrastructure: 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
@Transactionaldo 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 aPessoaé 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
CoordenadaGeograficanã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ó umaStringcomo 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;
}
// ...
}
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
}
}
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
Commandexecutado — 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 deexecutar()), um executor pode manter duas pilhas —feitasedesfeitas— empilhando cada comando executado na primeira; ao desfazer, desempilha da primeira, chamadesfazer(), 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.
Encerrando o catálogo¶
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.