EstruturalIntermediário7 min de leitura9 seções1994, Marco de referência: a ideia é mais velha que a data

Decorator

Adiciona comportamento a um objeto em tempo de execução envolvendo-o em camadas empilháveis que compartilham a mesma interface — composição no lugar de herança.

Em 10 segundos

Envolva o objeto em camadas que implementam a mesma interface: cada camada adiciona um comportamento e delega ao interno. Você compõe responsabilidades em runtime, sem tocar na classe original nem criar uma subclasse por combinação.

  • #composicao
  • #wrapper
  • #runtime
  • #gof
Analogia

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.

01Conceito

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.

Como se encaixa
  1. Cliente
  2. enviar(msg)ComAuditoria
  3. registra audit e delegaComSMS
  4. manda SMS e delegaNotificadorEmail
A chamada atravessa as camadas: cada decorator faz seu extra e repassa — o cliente vê uma interface só, sem saber quantas camadas existem.
Como se organiza
  • ComAuditoriacasca externa — registra e delega

    • ComSMSmanda SMS e delega

      • NotificadorEmailo objeto real, intocado

A mesma pilha vista por dentro: cada decorator embrulha o anterior, e o objeto original fica no centro sem ter sido modificado. Trocar a ordem das cascas troca a ordem dos efeitos.
Passo a passoO fluxo, passo a passo
  1. 1

    Contrato comum

    Componente e decorators implementam a mesma interface — são intercambiáveis.

  2. 2

    Uma responsabilidade por camada

    Cada decorator adiciona um extra e delega ao interno.

  3. 3

    Empilhar em runtime

    Monte a ordem na composição: new ComA(new ComB(base)).

  4. 4

    Cliente chama a interface

    Quem usa não sabe quantas camadas existem — só o contrato.

02Anatomia

Navegue pelas camadas

Camada 1/4Interface comum

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.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

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");
05No mundo real

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 500 transitó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;
  }
}
08Cuidado

Armadilhas

  1. 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.

  2. 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.

  3. 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.

09Decisão

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.

Costuma ser confundido com

Onde isto aparece

Continue por aqui