Pular para o conteúdo

O Decorator é um padrão de projeto estrutural que permite adicionar novos comportamentos a um objeto sem alterar sua classe original. Em vez de criar muitas subclasses para cada variação de comportamento, o padrão encapsula o objeto dentro de outros objetos que implementam a mesma interface.

Esse padrão existe para lidar com situações em que as funcionalidades extras são opcionais, combináveis e podem variar em tempo de execução. Ele aparece bastante em bibliotecas, interfaces gráficas, streams de entrada e saída, sistemas de notificação e pipelines de processamento.

Imagine uma biblioteca de notificações usada por vários sistemas. No início, ela envia apenas emails. Depois, surgem novas necessidades:

  • alguns clientes querem SMS
  • outros querem notificação por push
  • clientes corporativos querem integração com Slack
  • alguns cenários exigem combinar mais de um canal ao mesmo tempo

Sem um desenho adequado, a equipe tende a criar subclasses como:

  • NotificadorEmail
  • NotificadorEmailESms
  • NotificadorEmailEPush
  • NotificadorEmailESmsESlack

Esse caminho gera problemas práticos:

  • explosão no número de subclasses
  • dificuldade para combinar comportamentos livremente
  • herança rígida demais para necessidades opcionais
  • alto acoplamento entre variações de comportamento
  • manutenção mais cara conforme os canais crescem

O problema central não é apenas adicionar funcionalidades. O problema é combinar responsabilidades extras sem transformar a hierarquia de classes em um labirinto.

O Decorator resolve isso usando composição em vez de depender apenas de herança.

  1. Defina uma interface comum, como Notificador.
  2. Crie um componente concreto com o comportamento básico, como envio por email.
  3. Crie um decorador base que também implemente Notificador e armazene outro Notificador internamente.
  4. Crie decoradores concretos, como SmsDecorator e SlackDecorator.
  5. Cada decorador adiciona comportamento antes ou depois de delegar para o objeto encapsulado.

Assim, o cliente pode montar pilhas de comportamentos dinamicamente. Por exemplo, um notificador pode enviar email, depois SMS e depois Slack, tudo sem mudar a interface usada pelo restante do sistema.

O ponto central do padrão é este: o cliente continua enxergando um único contrato, mesmo quando vários comportamentos extras estão empilhados.

Pense em roupas no inverno. A pessoa continua sendo a mesma, mas pode vestir um suéter, depois um casaco, depois uma capa de chuva. Cada camada adiciona uma proteção extra sem mudar quem a pessoa é.

No Decorator, o objeto base é como a pessoa. Os decoradores são as camadas adicionadas por fora. Cada nova camada amplia o comportamento sem trocar a identidade principal do objeto.

Use Decorator quando:

  • você precisa adicionar responsabilidades opcionais a objetos
  • diferentes combinações de comportamento são necessárias
  • herança geraria muitas subclasses
  • o comportamento extra deve poder ser ligado ou removido dinamicamente
  • o cliente deve continuar usando a mesma interface

Exemplos reais em software moderno:

  • notificadores com múltiplos canais de envio
  • streams de entrada e saída com buffer, compressão ou criptografia
  • componentes visuais com borda, rolagem ou sombra
  • middlewares e pipelines de processamento em APIs
  • bibliotecas que empilham validações, logs ou métricas

Decorator pode ser mal utilizado quando o sistema passa a empilhar muitas camadas sem clareza. Alguns erros comuns são:

  • criar decoradores para comportamentos que deveriam estar no componente base
  • montar cadeias profundas demais, difíceis de rastrear
  • usar decoradores quando a ordem das camadas gera efeitos colaterais inesperados
  • confundir Decorator com Adapter ou Proxy apenas porque a estrutura parece parecida

As consequências incluem depuração mais difícil, comportamento dependente da ordem de empilhamento e objetos mais complicados de entender.

Decorator funciona melhor quando cada camada tem uma responsabilidade clara, pequena e previsível.

Uma forma incremental de implementar Decorator é:

  1. Identifique a interface comum usada pelo cliente.
  2. Crie o componente concreto com o comportamento mínimo necessário.
  3. Crie um decorador base com uma referência para outro componente da mesma interface.
  4. Faça o decorador base delegar as operações para o objeto encapsulado.
  5. Crie decoradores concretos para cada responsabilidade adicional.
  6. No código cliente, monte a pilha de decoradores conforme a necessidade.

Antes de escrever o código, vale responder: essas funcionalidades extras são realmente independentes e combináveis? Se a resposta for sim, Decorator costuma ser uma boa escolha.

No exemplo abaixo, um sistema de notificações combina canais de envio sem criar várias subclasses especializadas.

interface Notificador {
void enviar(String mensagem);
}
class EmailNotificador implements Notificador {
@Override
public void enviar(String mensagem) {
System.out.println("Email enviado: " + mensagem);
}
}
abstract class NotificadorDecorator implements Notificador {
protected Notificador wrappee;
public NotificadorDecorator(Notificador wrappee) {
this.wrappee = wrappee;
}
@Override
public void enviar(String mensagem) {
wrappee.enviar(mensagem);
}
}
class SmsDecorator extends NotificadorDecorator {
public SmsDecorator(Notificador wrappee) {
super(wrappee);
}
@Override
public void enviar(String mensagem) {
super.enviar(mensagem);
System.out.println("SMS enviado: " + mensagem);
}
}

Nesse código, EmailNotificador oferece o comportamento base. NotificadorDecorator encapsula outro Notificador e delega a ele. SmsDecorator adiciona uma nova responsabilidade sem alterar a implementação original.

O cliente pode continuar usando apenas a interface Notificador, sem saber quantas camadas foram aplicadas.

  • adiciona comportamentos sem alterar classes existentes
  • evita explosão de subclasses
  • permite combinar responsabilidades de forma flexível
  • respeita bem o princípio aberto/fechado
  • favorece composição em vez de herança rígida
  • pode gerar muitas classes pequenas
  • dificulta a depuração quando há várias camadas
  • a ordem dos decoradores pode afetar o resultado
  • a configuração inicial pode ficar verbosa

O Decorator se relaciona com outros padrões importantes:

  • Adapter: Adapter muda a interface percebida; Decorator preserva a interface e adiciona comportamento
  • Proxy: Proxy controla acesso mantendo a mesma interface; Decorator amplia responsabilidades do objeto encapsulado
  • Composite: ambos usam composição recursiva, mas Decorator costuma encapsular um único componente, enquanto Composite organiza vários filhos
  • Strategy: Strategy troca o algoritmo interno; Decorator adiciona camadas externas de comportamento

Essas comparações ajudam a perceber que Decorator não existe para traduzir contratos nem para controlar acesso, mas para estender comportamento de forma composicional.

As formas mais comuns de aplicar Decorator são:

  • decoradores para notificação, log, cache ou validação
  • wrappers em bibliotecas de I/O, como buffer e compressão
  • decoradores de interface gráfica, como borda e barra de rolagem
  • middlewares encadeados em frameworks web

Em Java, o padrão aparece com frequência em classes da biblioteca de streams, nas quais um fluxo pode ser envolvido por outro para ganhar novas capacidades.

Agora veja um caso mais realista em um sistema acadêmico. A universidade precisa avisar alunos sobre cancelamento de aula. Algumas turmas recebem apenas email, enquanto outras recebem email e SMS.

interface Notificador {
void enviar(String mensagem);
}
class EmailNotificador implements Notificador {
@Override
public void enviar(String mensagem) {
System.out.println("Email para alunos: " + mensagem);
}
}
abstract class NotificadorDecorator implements Notificador {
protected Notificador wrappee;
public NotificadorDecorator(Notificador wrappee) {
this.wrappee = wrappee;
}
@Override
public void enviar(String mensagem) {
wrappee.enviar(mensagem);
}
}
class SmsDecorator extends NotificadorDecorator {
public SmsDecorator(Notificador wrappee) {
super(wrappee);
}
@Override
public void enviar(String mensagem) {
super.enviar(mensagem);
System.out.println("SMS para alunos: " + mensagem);
}
}
class SlackDecorator extends NotificadorDecorator {
public SlackDecorator(Notificador wrappee) {
super(wrappee);
}
@Override
public void enviar(String mensagem) {
super.enviar(mensagem);
System.out.println("Mensagem no Slack da coordenação: " + mensagem);
}
}
public class PortalAcademico {
public static void main(String[] args) {
Notificador notificador = new SlackDecorator(
new SmsDecorator(
new EmailNotificador()
)
);
notificador.enviar("A aula de hoje foi transferida para o laboratório 4.");
}
}

Nesse exemplo, o cliente trabalha sempre com Notificador. A pilha de decoradores decide quantos canais serão acionados, sem exigir subclasses para cada combinação possível.

Esse tipo de modelagem funciona bem quando o sistema precisa adicionar comportamentos opcionais e encadeáveis, mantendo o código cliente simples.