Como implementar o padrão Decorator e em quais situações utilizá-lo

Question152

Tagsdesign-patterns, decorator, gof, structural-patterns, oop, extensibility, composition, behavioral

Introdução

O padrão Decorator é um padrão estrutural que permite adicionar novos comportamentos a objetos existentes de forma dinâmica, envolvendo-os em objetos especiais que contêm os comportamentos. É uma alternativa flexível à herança para estender funcionalidades.

Conceito-chave

O Decorator opera no princípio de composição sobre herança, permitindo que comportamentos sejam anexados e removidos de objetos em tempo de execução. Ele mantém a mesma interface do objeto original, mas adiciona responsabilidades adicionais através de uma cadeia de decoradores.

Tópicos Relevantes

Estrutura do padrão

// Component - interface ou classe abstrata
public interface IComponent
{
    string Operation();
}

// ConcreteComponent - implementação base
public class ConcreteComponent : IComponent
{
    public virtual string Operation()
    {
        return "ConcreteComponent";
    }
}

// Decorator base
public abstract class BaseDecorator : IComponent
{
    protected IComponent _component;

    public BaseDecorator(IComponent component)
    {
        _component = component;
    }

    public virtual string Operation()
    {
        return _component?.Operation() ?? string.Empty;
    }
}

// ConcreteDecorators - implementações específicas
public class ConcreteDecoratorA : BaseDecorator
{
    public ConcreteDecoratorA(IComponent component) : base(component) { }

    public override string Operation()
    {
        return $"ConcreteDecoratorA({base.Operation()})";
    }
}

public class ConcreteDecoratorB : BaseDecorator
{
    public ConcreteDecoratorB(IComponent component) : base(component) { }

    public override string Operation()
    {
        return $"ConcreteDecoratorB({base.Operation()})";
    }

    public string ExtraFeature()
    {
        return "ExtraFeature";
    }
}

Exemplo prático - Sistema de notificações

// Interface base para notificações
public interface INotifier
{
    void Send(string message);
}

// Implementação básica por email
public class EmailNotifier : INotifier
{
    private string email;

    public EmailNotifier(string email)
    {
        this.email = email;
    }

    public void Send(string message)
    {
        Console.WriteLine($"Enviando email para {email}: {message}");
    }
}

// Decorator base
public abstract class NotifierDecorator : INotifier
{
    protected INotifier notifier;

    public NotifierDecorator(INotifier notifier)
    {
        this.notifier = notifier;
    }

    public virtual void Send(string message)
    {
        notifier.Send(message);
    }
}

// Decorator para SMS
public class SMSDecorator : NotifierDecorator
{
    private string phoneNumber;

    public SMSDecorator(INotifier notifier, string phoneNumber) 
        : base(notifier)
    {
        this.phoneNumber = phoneNumber;
    }

    public override void Send(string message)
    {
        base.Send(message);
        Console.WriteLine($"Enviando SMS para {phoneNumber}: {message}");
    }
}

// Decorator para Slack
public class SlackDecorator : NotifierDecorator
{
    private string slackChannel;

    public SlackDecorator(INotifier notifier, string slackChannel) 
        : base(notifier)
    {
        this.slackChannel = slackChannel;
    }

    public override void Send(string message)
    {
        base.Send(message);
        Console.WriteLine($"Enviando para Slack #{slackChannel}: {message}");
    }
}

// Decorator para Discord
public class DiscordDecorator : NotifierDecorator
{
    private string discordChannel;

    public DiscordDecorator(INotifier notifier, string discordChannel) 
        : base(notifier)
    {
        this.discordChannel = discordChannel;
    }

    public override void Send(string message)
    {
        base.Send(message);
        Console.WriteLine($"Enviando para Discord #{discordChannel}: {message}");
    }
}

Uso do sistema de notificações

public class NotificationService
{
    public void SendUrgentNotification(string message)
    {
        // Notificação básica por email
        INotifier notifier = new EmailNotifier("admin@company.com");

        // Adicionar SMS para notificações urgentes
        notifier = new SMSDecorator(notifier, "+5511999999999");

        // Adicionar Slack para equipe de desenvolvimento
        notifier = new SlackDecorator(notifier, "dev-alerts");

        // Adicionar Discord para comunidade
        notifier = new DiscordDecorator(notifier, "general");

        notifier.Send(message);
    }

    public void SendRegularNotification(string message)
    {
        // Apenas email para notificações regulares
        INotifier notifier = new EmailNotifier("team@company.com");
        notifier.Send(message);
    }
}

Exemplo Prático

Sistema de processamento de dados com diferentes tipos de transformações:

// Interface para processamento de dados
public interface IDataProcessor
{
    string Process(string data);
}

// Processador básico
public class BasicDataProcessor : IDataProcessor
{
    public string Process(string data)
    {
        return data;
    }
}

// Decorator base
public abstract class DataProcessorDecorator : IDataProcessor
{
    protected IDataProcessor processor;

    public DataProcessorDecorator(IDataProcessor processor)
    {
        this.processor = processor;
    }

    public virtual string Process(string data)
    {
        return processor.Process(data);
    }
}

// Decorator para criptografia
public class EncryptionDecorator : DataProcessorDecorator
{
    public EncryptionDecorator(IDataProcessor processor) : base(processor) { }

    public override string Process(string data)
    {
        string processedData = base.Process(data);
        return Encrypt(processedData);
    }

    private string Encrypt(string data)
    {
        // Simulação de criptografia
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(data));
    }
}

// Decorator para compressão
public class CompressionDecorator : DataProcessorDecorator
{
    public CompressionDecorator(IDataProcessor processor) : base(processor) { }

    public override string Process(string data)
    {
        string processedData = base.Process(data);
        return Compress(processedData);
    }

    private string Compress(string data)
    {
        // Simulação de compressão
        return $"COMPRESSED[{data}]";
    }
}

// Decorator para logging
public class LoggingDecorator : DataProcessorDecorator
{
    private readonly ILogger logger;

    public LoggingDecorator(IDataProcessor processor, ILogger logger) 
        : base(processor)
    {
        this.logger = logger;
    }

    public override string Process(string data)
    {
        logger.Log($"Processando dados: {data.Substring(0, Math.Min(50, data.Length))}...");
        
        var startTime = DateTime.Now;
        string result = base.Process(data);
        var duration = DateTime.Now - startTime;
        
        logger.Log($"Processamento concluído em {duration.TotalMilliseconds}ms");
        return result;
    }
}

// Decorator para validação
public class ValidationDecorator : DataProcessorDecorator
{
    public ValidationDecorator(IDataProcessor processor) : base(processor) { }

    public override string Process(string data)
    {
        ValidateInput(data);
        string result = base.Process(data);
        ValidateOutput(result);
        return result;
    }

    private void ValidateInput(string data)
    {
        if (string.IsNullOrEmpty(data))
            throw new ArgumentException("Dados de entrada não podem ser nulos ou vazios");
    }

    private void ValidateOutput(string data)
    {
        if (string.IsNullOrEmpty(data))
            throw new InvalidOperationException("Processamento resultou em dados inválidos");
    }
}

// Uso combinado dos decorators
public class DataProcessingService
{
    private readonly ILogger logger;

    public DataProcessingService(ILogger logger)
    {
        this.logger = logger;
    }

    public string ProcessSensitiveData(string data)
    {
        IDataProcessor processor = new BasicDataProcessor();
        
        // Adicionar validação
        processor = new ValidationDecorator(processor);
        
        // Adicionar logging
        processor = new LoggingDecorator(processor, logger);
        
        // Adicionar compressão
        processor = new CompressionDecorator(processor);
        
        // Adicionar criptografia
        processor = new EncryptionDecorator(processor);

        return processor.Process(data);
    }

    public string ProcessRegularData(string data)
    {
        IDataProcessor processor = new BasicDataProcessor();
        
        // Apenas validação e logging para dados regulares
        processor = new ValidationDecorator(processor);
        processor = new LoggingDecorator(processor, logger);

        return processor.Process(data);
    }
}

Benefícios

1. Flexibilidade em tempo de execução

  • Permite adicionar/remover comportamentos dinamicamente
  • Não requer modificação do código existente
  • Configuração flexível de funcionalidades

2. Princípio da Responsabilidade Única

  • Cada decorator tem uma responsabilidade específica
  • Facilita manutenção e teste individual
  • Promove código mais limpo e organizado

3. Composição sobre herança

  • Evita hierarquias complexas de classes
  • Permite combinações ilimitadas de comportamentos
  • Reduz acoplamento entre classes

4. Extensibilidade

  • Novos decorators podem ser adicionados facilmente
  • Não afeta decorators existentes
  • Facilita evolução do sistema

5. Reutilização

  • Decorators podem ser reutilizados em diferentes contextos
  • Promove desenvolvimento modular
  • Reduz duplicação de código

Situações de uso

Quando utilizar:

  • Adicionar responsabilidades a objetos de forma dinâmica
  • Quando herança resultaria em muitas subclasses
  • Implementar funcionalidades opcionais ou configuráveis
  • Sistemas de middleware ou pipelines de processamento
  • Logging, caching, validação, autenticação transversais

Quando evitar:

  • Quando a interface do objeto muda frequentemente
  • Para funcionalidades fundamentais do objeto
  • Quando performance é crítica (overhead de chamadas)
  • Em sistemas muito simples onde herança seria suficiente