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 LoggingDecorator pode registrar chamadas de método, tempo de execução e erros.

  • Cache e validação: Um CacheDecorator verifica se o resultado já está em cache antes de executar a operação real. Um ValidationDecorator valida 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