Padrão Estrutural (GoF)

Decorator

Anexa responsabilidades adicionais a um objeto dinamicamente, envolvendo-o em um wrapper que implementa a mesma interface — uma alternativa flexível à herança para estender comportamento.

Intenção

Adicionar responsabilidades a um objeto de forma dinâmica, sem alterar sua classe. O Decorator envolve o objeto original em um wrapper que implementa a mesma interface — delegando as chamadas ao objeto interno e adicionando comportamento antes, depois ou ao redor da delegação. Decorators podem ser empilhados em qualquer ordem e combinação.

Catalogado pelo GoF (1994) como padrão estrutural, o Decorator é a resposta elegante ao problema da explosão de subclasses: quando você precisa de combinações variadas de comportamento, criar uma subclasse para cada combinação escala de forma insustentável.

Problema

Considere um sistema de notificações que precisa suportar funcionalidades opcionais: adicionar timestamp à mensagem, registrar a notificação em um log, e criptografar o conteúdo antes de enviar. Essas funcionalidades podem ser combinadas livremente — com ou sem timestamp, com ou sem log, com ou sem criptografia.

Pela abordagem de herança, você precisaria de uma subclasse para cada combinação: NotificadorComTimestamp, NotificadorComLog, NotificadorComTimestampELog, NotificadorComCriptografia, NotificadorComTimestampECriptografia, e assim por diante. Com apenas três funcionalidades opcionais, são até 8 combinações possíveis (2³). Com quatro, são 16. O crescimento é exponencial.

Além disso, herança é estática — a combinação de comportamentos é fixada em tempo de compilação, não pode ser alterada em runtime conforme as necessidades mudam.

Decorator vs Adapter: a diferença-chave

É importante não confundir os dois padrões. O Adapter muda a interface de um objeto — seu objetivo é compatibilidade entre interfaces incompatíveis. O Decorator mantém exatamente a mesma interface do objeto que envolve — seu objetivo é adicionar comportamento ao mesmo contrato. Se você vê um wrapper que muda a interface, é Adapter; se mantém a interface e acrescenta funcionalidade, é Decorator.

Solução

O Decorator organiza o código em quatro participantes:

  1. Component (interface): define o contrato compartilhado entre o objeto concreto e todos os decorators. Ex.: interface Notificador com o método enviar(mensagem).
  2. ConcreteComponent: a implementação base, sem decorações. Ex.: NotificadorEmail — envia o e-mail e só.
  3. Decorator (classe base abstrata, opcional): implementa Component e mantém uma referência a outro Component (o objeto envolvido). Delega todas as chamadas ao objeto interno. Em TypeScript, pode ser uma classe abstrata ou simplesmente cada decorator concreto manter a referência.
  4. ConcreteDecorator: estende o Decorator e adiciona comportamento antes, depois ou ao redor da chamada delegada. Ex.: NotificadorComTimestamp, NotificadorComLog.

A chave é que o resultado de um decorator é ele mesmo um Component — o que permite empilhar decorators livremente: new NotificadorComLog(new NotificadorComTimestamp(new NotificadorEmail())).

Estrutura

         «interface»
          Notificador
    ┌──────────────────────────┐
    │ + enviar(msg): void      │
    └──────────────────────────┘
          ▲          ▲
          │          │ implementa (e envolve outro Notificador)
NotificadorEmail  NotificadorDecorator (base abstrata)
(ConcreteComp.)   ┌──────────────────────────────────┐
                  │ # wrapped: Notificador            │
                  │ + enviar(msg): void               │
                  │   → delega para this.wrapped      │
                  └──────────────────────────────────┘
                                ▲
                    ┌───────────┴────────────────┐
                    │                            │
          NotificadorComTimestamp     NotificadorComLog
          (ConcreteDecorator)         (ConcreteDecorator)
          enviar(msg):                enviar(msg):
            msg = "[ts] " + msg        this.wrapped.enviar(msg)
            this.wrapped.enviar(msg)   registrarLog(msg)


Empilhamento (da direita para a esquerda no código):

  new NotificadorComLog(
    new NotificadorComTimestamp(
      new NotificadorEmail()
    )
  )

  → enviar("Olá") chama:
    1. NotificadorComLog.enviar       → registra no log
    2. NotificadorComTimestamp.enviar → prepend timestamp
    3. NotificadorEmail.enviar        → envia o e-mail

Exemplos de código

Exemplo 1 — Notificador com camadas opcionais

Cada decorator adiciona uma responsabilidade. A combinação e a ordem são definidas em tempo de execução — sem criar subclasses para cada combinação.

// ── Component ─────────────────────────────────────────────────
interface Notificador {
  enviar(mensagem: string): void;
}

// ── ConcreteComponent ─────────────────────────────────────────
class NotificadorEmail implements Notificador {
  enviar(mensagem: string): void {
    console.log(`[EMAIL] ${mensagem}`);
  }
}

// ── Decorators ────────────────────────────────────────────────

// Adiciona timestamp à mensagem antes de delegar.
class NotificadorComTimestamp implements Notificador {
  constructor(private readonly wrapped: Notificador) {}

  enviar(mensagem: string): void {
    const ts = new Date().toISOString();
    this.wrapped.enviar(`[${ts}] ${mensagem}`);
  }
}

// Registra a mensagem em log antes de delegar.
class NotificadorComLog implements Notificador {
  private readonly historico: string[] = [];

  constructor(private readonly wrapped: Notificador) {}

  enviar(mensagem: string): void {
    this.historico.push(mensagem);
    console.log(`[LOG] Mensagem registrada. Total: ${this.historico.length}`);
    this.wrapped.enviar(mensagem);
  }

  getHistorico(): readonly string[] {
    return this.historico;
  }
}

// Simula criptografia simples antes de delegar.
class NotificadorComCriptografia implements Notificador {
  constructor(private readonly wrapped: Notificador) {}

  enviar(mensagem: string): void {
    // Simulação: invertemos a string como "criptografia"
    const cifrada = mensagem.split("").reverse().join("");
    this.wrapped.enviar(`[ENC:${cifrada}]`);
  }
}

// ── Uso — compondo decorators em runtime ──────────────────────
const base = new NotificadorEmail();

// Apenas com timestamp:
const comTimestamp = new NotificadorComTimestamp(base);
comTimestamp.enviar("Sua fatura chegou");
// [EMAIL] [2026-06-25T...] Sua fatura chegou

// Com log + timestamp (log é o mais externo):
const loggerDecorado = new NotificadorComLog(
  new NotificadorComTimestamp(base)
);
loggerDecorado.enviar("Pedido confirmado");
// [LOG] Mensagem registrada. Total: 1
// [EMAIL] [2026-06-25T...] Pedido confirmado

// Todos os três, na ordem: log → criptografia → timestamp → email
const completo = new NotificadorComLog(
  new NotificadorComCriptografia(
    new NotificadorComTimestamp(base)
  )
);
completo.enviar("Ola");
// [LOG] Mensagem registrada. Total: 1
// [EMAIL] [2026-06-25T...] [ENC:alO]

Exemplo 2 — Preço de bebida com adicionais (exemplo clássico GoF)

O exemplo do café com adicionais é canônico na literatura sobre Decorator. Cada adicional (leite, caramelo, chantilly) é um decorator que soma seu preço ao total do componente envolvido. O resultado final acumula corretamente todos os valores.

// ── Component ─────────────────────────────────────────────────
interface Bebida {
  descricao(): string;
  preco(): number; // em centavos
}

// ── ConcreteComponents ────────────────────────────────────────
class Espresso implements Bebida {
  descricao(): string { return "Espresso"; }
  preco(): number     { return 300; } // R$3,00
}

class Americano implements Bebida {
  descricao(): string { return "Americano"; }
  preco(): number     { return 200; } // R$2,00
}

// ── Decorators de adicional ───────────────────────────────────
class ComLeite implements Bebida {
  constructor(private readonly bebida: Bebida) {}
  descricao(): string { return `${this.bebida.descricao()}, Leite`; }
  preco(): number     { return this.bebida.preco() + 50; } // +R$0,50
}

class ComCaramelo implements Bebida {
  constructor(private readonly bebida: Bebida) {}
  descricao(): string { return `${this.bebida.descricao()}, Caramelo`; }
  preco(): number     { return this.bebida.preco() + 75; } // +R$0,75
}

class ComChantilly implements Bebida {
  constructor(private readonly bebida: Bebida) {}
  descricao(): string { return `${this.bebida.descricao()}, Chantilly`; }
  preco(): number     { return this.bebida.preco() + 100; } // +R$1,00
}

// ── Uso ───────────────────────────────────────────────────────
const simples = new Espresso();
console.log(`${simples.descricao()}: R$${(simples.preco() / 100).toFixed(2)}`);
// Espresso: R$3.00

const capuccino = new ComChantilly(new ComLeite(new Espresso()));
console.log(`${capuccino.descricao()}: R$${(capuccino.preco() / 100).toFixed(2)}`);
// Espresso, Leite, Chantilly: R$4.50
// (300 + 50 + 100 = 450 centavos = R$4,50 ✓)

const especial = new ComCaramelo(new ComChantilly(new ComLeite(new Americano())));
console.log(`${especial.descricao()}: R$${(especial.preco() / 100).toFixed(2)}`);
// Americano, Leite, Chantilly, Caramelo: R$4.25
// (200 + 50 + 100 + 75 = 425 centavos = R$4,25 ✓)

Quando usar

  • Quando você precisa adicionar responsabilidades a objetos de forma dinâmica e reversível, sem modificar a classe original.
  • Para evitar a explosão de subclasses em cenários onde comportamentos são opcionais e combinados livremente (ex.: middleware de logging + autenticação + cache).
  • Quando a extensão via herança é impraticável porque a classe é final, ou porque as combinações possíveis são demais para modelar estaticamente.
  • Pipelines e middlewares: a pilha de middlewares em Express, Laravel, ASP.NET e similares é uma aplicação direta do Decorator — cada middleware é um decorator sobre o handler seguinte.

Quando evitar

  • Quando há um único comportamento adicional fixo: subclasse simples ou composição direta é mais legível do que criar a estrutura completa de Decorator.
  • Quando a ordem dos decorators é sensível e não documentada: pilhas de decorators podem gerar comportamentos inesperados se montadas na ordem errada. Se isso for frequente, considere um Builder para construir a pilha de forma controlada.
  • Quando você precisa referenciar o componente concreto: código que faz downcasting para obter o ConcreteComponent de dentro da pilha de decorators quebra o encapsulamento e invalida o padrão.

Prós e contras

Prós

  • Estende o comportamento de objetos sem alterar a classe original (Princípio Aberto/Fechado).
  • Combina responsabilidades em runtime — muito mais flexível do que herança estática.
  • Evita hierarquias de classes enormes para cobrir todas as combinações possíveis.
  • Cada decorator tem uma única responsabilidade — mais fácil de testar isoladamente.
  • Os decorators podem ser reutilizados em diferentes combinações e sobre diferentes components.

Contras

  • Pilhas profundas de decorators tornam a depuração difícil — rastrear qual decorator gerou um comportamento inesperado exige inspecionar toda a cadeia.
  • A ordem dos decorators importa e pode ser contraintuitiva.
  • Interfaces grandes (muitos métodos) são trabalhosas de decorar — cada decorator precisa implementar todos os métodos, mesmo que só sobreponha um.
  • Pode ser difícil remover um decorator específico do meio de uma pilha.

Armadilhas comuns

1. Confundir Decorator com Adapter

A distinção essencial: o Decorator mantém a mesma interface do objeto que envolve. Se o wrapper retorna uma interface diferente da do objeto envolvido, é um Adapter, não um Decorator. Quando você percebe que o wrapper está "traduzindo" chamadas para uma API diferente, reveja se o padrão correto não seria Adapter.

2. Decorators com estado compartilhado

Atenção: cada instância de decorator tem seu próprio estado (como o historico do NotificadorComLog). Se o mesmo decorator for reutilizado em contextos diferentes sem ser reinstanciado, o estado acumulado de um contexto pode vazar para outro. Prefira decorators stateless sempre que possível; quando estado for necessário, documente claramente o ciclo de vida esperado.

3. Interfaces grandes são difíceis de decorar

Se a interface Component tem 10 métodos, cada decorator deve implementar todos os 10 — mesmo que queira sobrepor apenas 1. Isso gera código repetitivo de delegação. A solução clássica é criar uma classe base abstrata de Decorator que delega todos os métodos ao wrapped, deixando os decorators concretos sobrescrever apenas o que precisam.

4. A ordem importa mais do que parece

No Exemplo 1, new NotificadorComLog(new NotificadorComTimestamp(base)) registra no log a mensagem sem timestamp (porque o log é o mais externo). Inverter a ordem — new NotificadorComTimestamp(new NotificadorComLog(base)) — faria o log registrar a mensagem com timestamp. Nem sempre é óbvio qual ordem é a desejada. Documente a ordem esperada e, se necessário, use um Builder para garantir a montagem correta.

Padrões relacionados

O Decorator interage com vários padrões, especialmente os outros estruturais:

O Adapter muda a interface; o Decorator mantém a interface e adiciona comportamento — é a diferença fundamental entre os dois. A Facade simplifica o acesso a um subsistema; um Facade pode ser internamente construído com Decorators sobre os componentes do subsistema. O Strategy também varia comportamento, mas por substituição: troca o algoritmo inteiro por um diferente (o contexto escolhe qual Strategy usar). O Decorator acumula comportamentos em camadas — não substitui, adiciona. O Composite compartilha a mesma estrutura recursiva (um objeto que contém outros do mesmo tipo), mas com o propósito de tratar um grupo de objetos como um só, não de adicionar responsabilidades. O Proxy também envolve um objeto mantendo a mesma interface, mas com o objetivo de controlar o acesso (lazy loading, cache, segurança) — não de adicionar responsabilidades de negócio.