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:
-
Component (interface): define o contrato compartilhado
entre o objeto concreto e todos os decorators. Ex.: interface
Notificadorcom o métodoenviar(mensagem). -
ConcreteComponent: a implementação base, sem decorações.
Ex.:
NotificadorEmail— envia o e-mail e só. - 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.
-
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]
<?php
// ── Component ─────────────────────────────────────────────────
interface Notificador
{
public function enviar(string $mensagem): void;
}
// ── ConcreteComponent ─────────────────────────────────────────
class NotificadorEmail implements Notificador
{
public function enviar(string $mensagem): void
{
echo "[EMAIL] {$mensagem}" . PHP_EOL;
}
}
// ── Decorators ────────────────────────────────────────────────
// Adiciona timestamp à mensagem antes de delegar.
class NotificadorComTimestamp implements Notificador
{
public function __construct(private readonly Notificador $wrapped) {}
public function enviar(string $mensagem): void
{
$ts = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM);
$this->wrapped->enviar("[{$ts}] {$mensagem}");
}
}
// Registra a mensagem em log antes de delegar.
class NotificadorComLog implements Notificador
{
/** @var string[] */
private array $historico = [];
public function __construct(private readonly Notificador $wrapped) {}
public function enviar(string $mensagem): void
{
$this->historico[] = $mensagem;
$total = count($this->historico);
echo "[LOG] Mensagem registrada. Total: {$total}" . PHP_EOL;
$this->wrapped->enviar($mensagem);
}
/** @return string[] */
public function getHistorico(): array
{
return $this->historico;
}
}
// Simula criptografia simples antes de delegar.
class NotificadorComCriptografia implements Notificador
{
public function __construct(private readonly Notificador $wrapped) {}
public function enviar(string $mensagem): void
{
$cifrada = strrev($mensagem);
$this->wrapped->enviar("[ENC:{$cifrada}]");
}
}
// ── Uso — compondo decorators em runtime ──────────────────────
$base = new NotificadorEmail();
// Apenas com timestamp:
$comTimestamp = new NotificadorComTimestamp($base);
$comTimestamp->enviar('Sua fatura chegou');
// [EMAIL] [2026-06-25T...] Sua fatura chegou
// Com log + timestamp:
$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:
$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 ✓)
<?php
// ── Component ─────────────────────────────────────────────────
interface Bebida
{
public function descricao(): string;
public function preco(): int; // em centavos
}
// ── ConcreteComponents ────────────────────────────────────────
class Espresso implements Bebida
{
public function descricao(): string { return 'Espresso'; }
public function preco(): int { return 300; } // R$3,00
}
class Americano implements Bebida
{
public function descricao(): string { return 'Americano'; }
public function preco(): int { return 200; } // R$2,00
}
// ── Decorators de adicional ───────────────────────────────────
class ComLeite implements Bebida
{
public function __construct(private readonly Bebida $bebida) {}
public function descricao(): string { return $this->bebida->descricao() . ', Leite'; }
public function preco(): int { return $this->bebida->preco() + 50; } // +R$0,50
}
class ComCaramelo implements Bebida
{
public function __construct(private readonly Bebida $bebida) {}
public function descricao(): string { return $this->bebida->descricao() . ', Caramelo'; }
public function preco(): int { return $this->bebida->preco() + 75; } // +R$0,75
}
class ComChantilly implements Bebida
{
public function __construct(private readonly Bebida $bebida) {}
public function descricao(): string { return $this->bebida->descricao() . ', Chantilly'; }
public function preco(): int { return $this->bebida->preco() + 100; } // +R$1,00
}
// ── Uso ───────────────────────────────────────────────────────
$simples = new Espresso();
printf("%s: R$%.2f\n", $simples->descricao(), $simples->preco() / 100);
// Espresso: R$3.00
$capuccino = new ComChantilly(new ComLeite(new Espresso()));
printf("%s: R$%.2f\n", $capuccino->descricao(), $capuccino->preco() / 100);
// Espresso, Leite, Chantilly: R$4.50
// (300 + 50 + 100 = 450 centavos = R$4,50 ✓)
$especial = new ComCaramelo(new ComChantilly(new ComLeite(new Americano())));
printf("%s: R$%.2f\n", $especial->descricao(), $especial->preco() / 100);
// 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
ConcreteComponentde 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.