Como usar o padrão decorator para adicionar comportamento sem herança
1. Introdução ao padrão Decorator
O padrão Decorator é uma solução elegante para um problema clássico da programação orientada a objetos: a explosão de subclasses. Quando precisamos adicionar comportamentos variados a um objeto, a herança tradicional leva a uma proliferação de classes combinatórias. Por exemplo, para um sistema de notificações, poderíamos ter NotificacaoEmail, NotificacaoSMS, NotificacaoEmailSMS, NotificacaoEmailSlack, e assim por diante — uma combinação para cada variação.
O Decorator resolve isso permitindo que você "empacote" um objeto base com camadas adicionais de comportamento em tempo de execução. Diferente da herança, que é estática e definida em tempo de compilação, a composição com Decorator é dinâmica: você decide quais comportamentos adicionar e em qual ordem, sem criar novas classes.
No nosso cenário prático, imagine um sistema de notificações que precisa enviar mensagens por email, SMS e Slack. Com herança, precisaríamos de 7 classes (Email, SMS, Slack, Email+SMS, Email+Slack, SMS+Slack, Email+SMS+Slack). Com Decorator, precisamos apenas de 4 classes (componente base + 3 decorators).
2. Estrutura fundamental do Decorator
O padrão é composto por quatro elementos principais:
- Componente (interface comum): Define o contrato que todos os objetos (base e decorados) devem implementar.
- ConcreteComponent: A implementação base do comportamento. É o objeto que será "envelopado".
- BaseDecorator: Classe abstrata que mantém uma referência ao componente e delega a execução.
- ConcreteDecorators: Classes que estendem o BaseDecorator e adicionam responsabilidades específicas antes ou depois da delegação.
A mágica está no fato de que tanto o ConcreteComponent quanto os ConcreteDecorators implementam a mesma interface, permitindo que decorators envolvam outros decorators recursivamente.
3. Implementação passo a passo em TypeScript
Vamos implementar nosso sistema de notificações. Primeiro, definimos a interface comum:
interface Notifier {
send(message: string): void;
}
Agora, o componente concreto que envia email:
class EmailNotifier implements Notifier {
send(message: string): void {
console.log(`Enviando email: ${message}`);
}
}
O decorator base é uma classe abstrata que mantém uma referência ao componente:
abstract class BaseDecorator implements Notifier {
protected wrapped: Notifier;
constructor(notifier: Notifier) {
this.wrapped = notifier;
}
send(message: string): void {
this.wrapped.send(message);
}
}
Agora, os decorators concretos:
class SMSDecorator extends BaseDecorator {
send(message: string): void {
super.send(message);
console.log(`Enviando SMS: ${message}`);
}
}
class SlackDecorator extends BaseDecorator {
send(message: string): void {
super.send(message);
console.log(`Enviando Slack: ${message}`);
}
}
Cada decorator chama o método do objeto encapsulado e adiciona seu próprio comportamento.
4. Empilhamento dinâmico de comportamentos
A verdadeira potência do Decorator aparece quando combinamos múltiplos decorators em tempo de execução:
const notifier = new SlackDecorator(
new SMSDecorator(
new EmailNotifier()
)
);
notifier.send("Promoção relâmpago!");
// Saída:
// Enviando email: Promoção relâmpago!
// Enviando SMS: Promoção relâmpago!
// Enviando Slack: Promoção relâmpago!
A ordem de execução vai do decorator mais externo (SlackDecorator) para o mais interno (EmailNotifier). Isso permite controlar a sequência de operações. Se quisermos que o SMS seja enviado antes do Slack, basta inverter a ordem:
const notifier2 = new SMSDecorator(
new SlackDecorator(
new EmailNotifier()
)
);
Com herança, precisaríamos criar classes como EmailSMSNotifier, EmailSlackNotifier, EmailSMSSlackNotifier — uma para cada combinação possível. Com Decorator, qualquer combinação é possível sem criar novas classes. Isso reduz drasticamente o acoplamento e a manutenção.
5. Casos de uso reais e aplicações
O padrão Decorator é amplamente utilizado em frameworks modernos:
-
Middleware em frameworks web: No Express.js, cada middleware é essencialmente um decorator que processa a requisição antes de passá-la adiante. No NestJS, os guards e interceptors seguem o mesmo princípio.
-
Logging e monitoramento: Adicione logs de entrada/saída sem modificar a lógica de negócio. Um
LoggingDecoratorpode registrar chamadas de método, tempo de execução e erros. -
Cache e validação: Um
CacheDecoratorverifica se o resultado já está em cache antes de executar a operação real. UmValidationDecoratorvalida parâmetros antes da execução. -
Segurança: Decorators de autenticação e autorização verificam permissões antes de permitir o acesso a recursos.
6. Boas práticas e armadilhas comuns
Algumas orientações importantes:
-
Mantenha a interface estável: Qualquer mudança na interface do componente quebra todos os decorators. Use interfaces pequenas e coesas.
-
Cuidado com a complexidade: Muitos decorators empilhados podem tornar o debug difícil. Documente a ordem esperada e evite mais de 3-4 camadas.
-
Quando evitar: Se o comportamento adicional for fixo ou a combinação for pequena (ex: sempre email + SMS), a herança pode ser mais simples. O Decorator brilha quando as combinações são muitas e variadas.
-
Decorator vs Strategy: O Strategy substitui um comportamento inteiro; o Decorator adiciona comportamento ao redor de um existente. São complementares, não concorrentes.
7. Comparação com alternativas
| Abordagem | Vantagens | Desvantagens |
|---|---|---|
| Herança | Simples para poucas variações | Explosão de classes, acoplamento rígido |
| Mixins | Reúso de código sem herança | Conflitos de nomes, complexidade |
| Composição pura | Flexibilidade total | Mais verboso, sem abstração padronizada |
| Decorator | Combinações dinâmicas, baixo acoplamento | Pode ser complexo para iniciantes |
O Decorator é ideal quando você precisa de muitas combinações de comportamentos e quer mantê-las independentes. Para comportamentos fixos ou hierarquias simples, a herança ainda é válida.
8. Conclusão e próximos passos
O padrão Decorator permite adicionar comportamento a objetos sem herança, usando composição dinâmica. Ele resolve o problema da explosão de subclasses e promove baixo acoplamento. Na lista de 1200 temas, ele se relaciona com outros padrões como Command (encapsular requisições) e Strategy (substituir algoritmos), formando um conjunto poderoso para design flexível.
Exercício prático: Implemente um sistema de desconto onde um pedido pode receber múltiplos descontos (fidelidade, cupom, sazonal) usando Decorator. O componente base calcula o preço original, e cada decorator aplica um desconto percentual.
Referências
- Decorator Pattern - Refactoring Guru — Explicação completa com diagramas UML e exemplos em várias linguagens.
- TypeScript Design Patterns: Decorator — Documentação oficial do TypeScript sobre decorators nativos e sua sintaxe.
- Decorator Pattern in Node.js — Tutorial prático aplicando o padrão em aplicações Node.js com exemplos de middleware.
- GoF Design Patterns: Decorator — Referência clássica do Gang of Four com explicação estrutural e casos de uso.
- NestJS: Custom Decorators — Como o NestJS implementa decorators para injeção de dependência e metadados.
- JavaScript Decorators: From Proposals to Practice — Artigo sobre a evolução dos decorators em JavaScript e TypeScript.