Pular para o conteúdo

O Decorator é um padrão de projeto estrutural que permite adicionar responsabilidades a um objeto dinamicamente, sem alterar o objeto nem criar uma classe para cada combinação. A técnica é envolver o objeto em outro que mantém a mesma interface e soma o próprio comportamento.

Ele aparece quando um sistema precisa combinar funcionalidades opcionais sobre uma base comum: um café com complementos, um serviço que pode ganhar log e cache, uma imagem que pode receber vários filtros. O Decorator evita que essas combinações virem uma explosão de subclasses.

Uma cafeteria vende bebidas e complementos combináveis. Por herança, cada combinação viraria uma subclasse nova:

  • CafeComLeite, CafeComChantilly, CafeComLeiteComChantilly…

Com N complementos, o número de combinações explode (2ᴺ). Além disso, a herança é estática:

  • a “decoração” fica fixa em tempo de compilação
  • mudar o preço base exige revisar todas as combinações
  • não dá para montar a mesma bebida com ordens diferentes
  • cada subclasse duplica a lógica de cálculo e de descrição

O Decorator resolve isso envolvendo o objeto em camadas:

  1. Defina uma interface comum (o Component).
  2. Crie o componente concreto: o objeto base, que implementa o comportamento.
  3. Crie o decorator: uma classe que também implementa a interface, guarda uma referência ao objeto interno e delega a ele.
  4. Crie decorators concretos que adicionam comportamento antes ou depois de delegar.

Cada camada se conecta como uma cadeia: o objeto externo delega ao interno e acrescenta algo. O cliente enxerga sempre a mesma interface, então pode empilhar quantas camadas quiser — em tempo de execução.

Pense em uma pizza montada. A massa é a base; os ingredientes são adicionados por cima. O “queijo extra” ou a “borda recheada” envolvem a pizza sem mudar o que ela é. No fim, continua sendo uma pizza — só que “decorada”.

Do outro lado, existe uma limitação natural: depois de muitas camadas, é difícil “enxergar” a massa original por baixo. No código, isso aparece quando precisamos acessar um comportamento específico do objeto interno após vários decorators.

Use Decorator quando:

  • você precisa adicionar responsabilidades individualmente e em combinações
  • a herança geraria explosão de subclasses ou é estática demais
  • quer adicionar comportamento em tempo de execução, com ordem livre
  • deseja compor funcionalidades (log, cache, compressão, cifragem) sem tocar na base

Exemplos reais em software moderno:

  • streams do java.io (BufferedInputStream em volta de FileInputStream)
  • Collections.unmodifiableList, Collections.synchronizedList
  • filtros de servlets, middlewares e interceptadores
  • adicionar cache, log, retry ou métricas a um serviço

Decorator pode ser mal utilizado quando a equipe o aplica sem necessidade. Os erros mais comuns são:

  • usar decorators quando uma subclasse simples resolveria (variação fixa, sem combinação)
  • empilhar camadas que dependem da ordem sem deixar isso explícito
  • criar decorators que precisam acessar o objeto interno, gerando instanceof e casts
  • decorar objetos demais e dificultar o rastreamento do comportamento

As consequências costumam ser muitas classes pequenas, dificuldade de depurar e um design mais complexo do que a simples extensão por herança resolveria.

Decorator funciona bem quando há combinação dinâmica de responsabilidades sobre uma interface estável.

Uma forma incremental de implementar Decorator é:

  1. Identifique a interface comum que o cliente já usa.
  2. Implemente o componente concreto com o comportamento base.
  3. Crie o decorator abstrato, que guarda uma referência ao componente e delega.
  4. Crie decorators concretos, adicionando responsabilidade antes ou depois da delegação.
  5. No cliente, monte a cadeia envolvendo o objeto nos decorators desejados.

Antes de começar, vale perguntar: as responsabilidades são combináveis em tempo de execução? A interface é estável? Se sim, Decorator é um forte candidato.

No exemplo abaixo, uma cafeteria soma complementos a uma bebida.

interface Bebida {
String getDescricao();
double custo();
}
class Cafe implements Bebida {
@Override
public String getDescricao() { return "Café"; }
@Override
public double custo() { return 5.0; }
}
abstract class Complemento implements Bebida {
protected final Bebida bebida;
public Complemento(Bebida bebida) {
this.bebida = bebida;
}
}
class Leite extends Complemento {
public Leite(Bebida bebida) { super(bebida); }
@Override
public String getDescricao() {
return bebida.getDescricao() + " com leite";
}
@Override
public double custo() {
return bebida.custo() + 1.5;
}
}
class Chantilly extends Complemento {
public Chantilly(Bebida bebida) { super(bebida); }
@Override
public String getDescricao() {
return bebida.getDescricao() + " com chantilly";
}
@Override
public double custo() {
return bebida.custo() + 2.0;
}
}

Nesse código, Cafe é o componente concreto e Complemento é o decorator abstrato. Leite e Chantilly são decorators concretos: cada um delega ao objeto interno e soma descrição e custo.

  • combinações montadas em tempo de execução, sem classes novas
  • novos decorators entram sem tocar no componente base (OCP)
  • cada responsabilidade fica isolada em um decorator (SRP)
  • as camadas podem ser empilhadas na ordem desejada
  • muitos objetos pequenos e camadas no sistema
  • a ordem dos decorators pode afetar o resultado
  • difícil inspecionar o objeto real sob as camadas
  • remover um complemento exige reconstruir a cadeia

O Decorator se relaciona com outros padrões importantes:

  • Composite: ambos são estruturais e usam composição. Composite agrega vários filhos formando uma árvore; Decorator envolve um objeto formando uma cadeia. O Decorator pode ser visto como um Composite “degenerado”, com um único filho.
  • Adapter: muda a interface de um objeto; o Decorator mantém a interface e adiciona comportamento.
  • Strategy: muda o comportamento interno (algoritmo); o Decorator muda o comportamento externo, envolvendo o objeto.
  • Factory Method / Abstract Factory: costumam trabalhar junto com Decorator para montar a cadeia de decorators.

Em muitas linguagens o Decorator aparece de outras formas:

  • em linguagens com suporte a decorators de linguagem (Python, TypeScript), a mesma ideia pode ser expressa de forma mais declarativa
  • BufferedReader/BufferedWriter no Java empilham funcionalidade sobre um stream base
  • middlewares e interceptadores (como filtros de servlets) aplicam a mesma lógica de cadeia sem herança

Independente da forma, a essência é a mesma: envolver um objeto com outro da mesma interface para somar responsabilidades dinamicamente.

Agora veja um caso mais realista com um serviço que precisa ganhar log e cache sem mudar a interface usada pelos consumidores.

interface ServicoProduto {
Produto buscar(int id);
}
class ServicoProdutoBase implements ServicoProduto {
@Override
public Produto buscar(int id) {
System.out.println("Buscando produto " + id + " no banco...");
return new Produto(id, "Cafeteira", 299.0);
}
}
class LogDecorator implements ServicoProduto {
private final ServicoProduto interno;
public LogDecorator(ServicoProduto interno) { this.interno = interno; }
@Override
public Produto buscar(int id) {
long inicio = System.currentTimeMillis();
Produto produto = interno.buscar(id);
System.out.println("Busca de " + id + " levou " +
(System.currentTimeMillis() - inicio) + " ms");
return produto;
}
}
class CacheDecorator implements ServicoProduto {
private final ServicoProduto interno;
private final Map<Integer, Produto> cache = new HashMap<>();
public CacheDecorator(ServicoProduto interno) { this.interno = interno; }
@Override
public Produto buscar(int id) {
if (cache.containsKey(id)) {
System.out.println("Cache hit para " + id);
return cache.get(id);
}
Produto produto = interno.buscar(id);
cache.put(id, produto);
return produto;
}
}
public class Loja {
public static void main(String[] args) {
ServicoProduto servico = new ServicoProdutoBase();
servico = new LogDecorator(servico);
servico = new CacheDecorator(servico);
servico.buscar(10); // vai ao banco e guarda no cache
servico.buscar(10); // cache hit
}
}

Nesse exemplo, ServicoProdutoBase é o componente concreto, e LogDecorator e CacheDecorator são decorators. O consumidor continua usando a interface ServicoProduto; combinar log e cache é só uma questão de montar a cadeia