Roupas em camadas
Num dia frio você veste camisa, suéter por cima e casaco por cima do suéter. Cada camada adiciona algo — aquecimento, impermeabilidade — sem mudar quem você é: por baixo continua a mesma pessoa, e você segue 'usável' do mesmo jeito (andar, falar, trabalhar). E a composição é sua, na hora: esfriou mais, adiciona o cachecol; entrou no prédio, tira o casaco. Nenhuma combinação exigiu 'nascer de novo' como outra pessoa — que é exatamente o que a herança exigiria.
O problema
Adicionar responsabilidades opcionais via herança explode em combinações: NotificadorComSMS, NotificadorComSMSEAuditoria, NotificadorComAuditoriaSemSMS... cada extra novo dobra o número de subclasses possíveis.
Pior: herança fixa o comportamento em tempo de compilação e para todas as instâncias da classe. Não há como montar a combinação certa para um objeto específico, num fluxo específico, em runtime.
A raiz do problema é que herança responde 'o que este objeto É', mas responsabilidades extras são sobre 'o que este objeto TEM a mais' — uma relação de composição, não de identidade. Quando extras opcionais são modelados como identidade, cada combinação vira um tipo novo, e a hierarquia cresce geometricamente.
Alternativas comuns falham de outros jeitos: flags booleanas na classe base (comSMS, comAuditoria) incham a classe com if's e violam aberto/fechado a cada extra novo; mixins resolvem parte, mas continuam decididos em tempo de definição da classe, não por instância.
O Decorator paga um preço honesto por essa flexibilidade: muitas classes pequenas e uma pilha de objetos para depurar. Compare com o Proxy — estruturalmente idêntico (wrapper com a mesma interface), mas com intenção diferente: o Proxy controla o acesso ao objeto (lazy, cache, permissão); o Decorator adiciona comportamento. E com o Adapter: o Adapter muda a interface para encaixar contratos incompatíveis; o Decorator preserva a interface e enriquece o comportamento.
- Cliente
- enviar(msg)ComAuditoria
- registra audit e delegaComSMS
- manda SMS e delegaNotificadorEmail
ComAuditoriacasca externa — registra e delega
ComSMSmanda SMS e delega
NotificadorEmailo objeto real, intocado
- 1
Contrato comum
Componente e decorators implementam a mesma interface — são intercambiáveis.
- 2
Uma responsabilidade por camada
Cada decorator adiciona um extra e delega ao interno.
- 3
Empilhar em runtime
Monte a ordem na composição: new ComA(new ComB(base)).
- 4
Cliente chama a interface
Quem usa não sabe quantas camadas existem — só o contrato.
Navegue pelas camadas
Camada 1/4Interface comum
use ← →Camada 1
Interface comum
o contrato que todos compartilham
Componente concreto e decorators implementam a mesma interface. É isso que torna as camadas intercambiáveis e empilháveis: qualquer decorator aceita qualquer Notificador — concreto ou já decorado.
No código
interface Notificador { enviar(msg: string): void; }Se violar: se o decorator expõe métodos próprios fora da interface, o cliente passa a depender da camada concreta e a pilha deixa de ser rearranjável.
Estrutura
Código
interface Notificador {
enviar(msg: string): void;
}
class NotificadorEmail implements Notificador {
enviar(msg: string): void {
console.log("Email: " + msg);
}
}
// Decorator base: implementa a interface E envolve outro Notificador
abstract class NotificadorDecorator implements Notificador {
constructor(protected interno: Notificador) {}
enviar(msg: string): void {
this.interno.enviar(msg);
}
}
class ComSMS extends NotificadorDecorator {
enviar(msg: string): void {
super.enviar(msg);
console.log("SMS: " + msg);
}
}
class ComAuditoria extends NotificadorDecorator {
enviar(msg: string): void {
console.log("[audit] notificacao registrada");
super.enviar(msg);
}
}
// Empilhando em runtime: cada camada adiciona algo sem mudar as demais
const notificador = new ComAuditoria(new ComSMS(new NotificadorEmail()));
notificador.enviar("Pedido aprovado");from abc import ABC, abstractmethod
class Notificador(ABC):
@abstractmethod
def enviar(self, msg: str) -> None: ...
class NotificadorEmail(Notificador):
def enviar(self, msg: str) -> None:
print(f"Email: {msg}")
# Decorator base: implementa a interface e envolve outro Notificador
class NotificadorDecorator(Notificador):
def __init__(self, interno: Notificador):
self._interno = interno
def enviar(self, msg: str) -> None:
self._interno.enviar(msg)
class ComSMS(NotificadorDecorator):
def enviar(self, msg: str) -> None:
super().enviar(msg)
print(f"SMS: {msg}")
class ComAuditoria(NotificadorDecorator):
def enviar(self, msg: str) -> None:
print("[audit] notificacao registrada")
super().enviar(msg)
# Empilhando em runtime
notificador = ComAuditoria(ComSMS(NotificadorEmail()))
notificador.enviar("Pedido aprovado")Casos de uso
Streams de I/O (o caso clássico do Java)
Cenário
Uma plataforma processa arquivos grandes: às vezes comprimidos, às vezes criptografados, às vezes ambos — e a combinação só é conhecida na hora de abrir o arquivo.
Como o padrão entra
A API java.io é Decorator puro: new GZIPInputStream(new BufferedInputStream(new FileInputStream(f))). Cada wrapper adiciona buffer, descompressão ou decriptação sobre o mesmo contrato InputStream, montado conforme o arquivo.
Trade-off: A montagem fica verbosa e a ordem importa (buffer por fora de gzip, não por dentro) — errar a ordem compila e roda, só que lento ou errado.
HTTP client com middlewares em SaaS
Cenário
Um SaaS integra com dezenas de APIs externas; algumas precisam de retry com backoff, outras de circuit breaker, todas de telemetria — mas não nas mesmas combinações.
Como o padrão entra
Um HttpClient base é envolvido por decorators ComRetry, ComTelemetria, ComCircuitBreaker. Cada integração monta sua pilha na configuração: a do gateway de pagamento leva retry + breaker; a de analytics, só telemetria.
Trade-off: Stack traces atravessam 4-5 camadas de wrappers, e um retry mal posicionado (por fora do breaker) pode martelar um serviço já caído.
Precificação de frete em logística
Cenário
Uma transportadora calcula frete base por peso/distância, e o cliente adiciona opcionais: seguro, entrega expressa, embalagem reforçada — qualquer combinação.
Como o padrão entra
CalculoFrete é decorado por ComSeguro, ComExpressa, ComEmbalagem: cada camada soma seu custo ao valor delegado. O carrinho monta a pilha conforme os checkboxes marcados, sem uma classe por combinação.
Trade-off: Quando os opcionais começam a interagir (seguro custa mais se for expressa), decorators independentes deixam de bastar — a regra cruzada pede outro modelo.
Decorators empilhados sem ordem definida
Compila, roda, passa no code review. E quebra depois.
Cada decorator foi escrito isolado e funciona. Empilhados, a ordem passa a importar de um jeito que ninguém documentou — e a montagem acontece em um arquivo de configuração que ninguém lê como código.
const cliente =
comCache(
comRetry(
comLog(
comAutenticacao(clienteBase))));
// Leia de dentro para fora: autentica -> loga -> repete -> cacheia.
// Consequencias que ninguem escreveu em lugar nenhum:
//
// 1. O cache esta FORA do retry: um erro cacheado nunca e repetido.
// 2. O log esta DENTRO do retry: 3 tentativas viram 3 linhas iguais,
// e o painel conta 3 requisicoes onde houve 1.
// 3. A autenticacao esta no fundo: o token e renovado a cada tentativa,
// inclusive nas que nem sairiam do cache.
// Trocar duas linhas de lugar muda o comportamento em producao
// sem mudar nenhum teste unitario — todos passam nos dois casos.O que quebra
- Ao inverter duas camadasO comportamento muda em produção e nenhum teste unitário acusa, porque cada decorator continua correto isolado.
- No diagnósticoA pilha de chamadas fica com dez quadros de embrulho antes do código real, e o erro aponta para o decorator, não para a causa.
- Ao cachear erroUm
500transitório entra no cache e passa a ser servido por minutos, mesmo depois de o serviço voltar.
A saída: A ordem é decisão de projeto e precisa estar escrita onde a pilha é montada, com o porquê de cada camada estar onde está. Regra prática: cache por fora do retry só se você aceitar cachear erro — normalmente o certo é o inverso.
Do cheiro ao padrão
Um passo por vez, com o motivo de cada um.
O cheiro: Uma flag booleana que virou quatro, e um construtor que aceita todas as combinações possíveis.
Como está. Uma flag booleana que virou quatro, e um construtor que aceita todas as combinações possíveis.
class ClienteHttp {
constructor(
private readonly comLog = false,
private readonly comCache = false,
private readonly comRetry = false,
) {}
async buscar(url: string) {
if (this.comCache && cache.has(url)) return cache.get(url);
if (this.comLog) console.log("GET", url);
let r;
if (this.comRetry) r = await comTentativas(() => fetch(url));
else r = await fetch(url);
if (this.comCache) cache.set(url, r);
return r;
}
}Armadilhas
A identidade do objeto muda
O objeto decorado NÃO é o original: instanceof NotificadorEmail falha, comparação por referência falha, e um unwrap acidental (alguém guardou a referência interna) fura todas as camadas. Se o código ao redor depende de identidade concreta, o Decorator quebra de formas silenciosas.
Ordem das camadas é semântica invisível
ComRetry(ComAuditoria(x)) audita cada tentativa; ComAuditoria(ComRetry(x)) audita só o resultado final. Os dois compilam e 'funcionam'. A ordem da pilha carrega significado que o compilador não checa — documente a montagem ou centralize-a numa factory.
Interface larga, decorator sofrido
Decorator sobre uma interface de 20 métodos obriga cada wrapper a delegar os 20 — e cada método novo na interface exige revisitar todos os decorators (esquecer um é bug silencioso). O padrão brilha sobre interfaces pequenas; sobre interfaces largas, considere segregá-las antes.
Quando usar × quando evitar
Use quando
3- Responsabilidades extras são opcionais e combináveis (log + retry + cache em qualquer arranjo).
- O comportamento precisa ser decidido em tempo de execução, por instância — não por classe.
- A classe base não pode ser alterada (código de terceiros, SDK fechado) mas aceita ser envolvida.
Evite quando
2- Só existe uma variação e ela é estável — uma subclasse ou um parâmetro resolve mais barato.
- O cliente depende da identidade concreta do objeto (instanceof, referência) — o wrapper quebra essas checagens.
Testar o que ficou
3 armadilhas de Decorator viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Middleware do ExpressCada middleware embrulha o próximo, acrescentando comportamento sem tocar no handler final.HOC do ReactwithRouter(Componente)devolve um componente novo com poderes extras e a mesma interface.@decorator do Python e do TSO padrão virou sintaxe da linguagem — a função sai embrulhada por outra.Pipes de stream do Node.pipe(gzip).pipe(cipher)empilha transformações que continuam sendo streams.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Mesma interface, comportamento a mais, empilhavel.
const cliente = comCache(comRetry(clienteBase));O que ele cobra
2 saltos a mais entre a chamada e o efeito.
- A ordem das camadas passa a importar e precisa estar documentada
- A pilha de chamadas ganha um quadro por camada
Não vale se: só existe uma camada e ela nunca é opcional — nesse caso o comportamento pertence ao próprio objeto.