Padrões estruturais: Decorator e Proxy

1. Introdução aos Padrões Decorator e Proxy

Os padrões estruturais Decorator e Proxy compartilham um objetivo arquitetural comum: adicionar ou controlar comportamento sem alterar a interface do objeto original. Ambos utilizam composição e implementam a mesma interface do objeto alvo, permitindo que clientes interajam com objetos decorados ou proxy como se fossem o objeto real.

A diferença fundamental reside na intenção: o Decorator adiciona responsabilidades dinamicamente, estendendo funcionalidades de forma transparente; o Proxy controla o acesso ao objeto real, gerenciando seu ciclo de vida, segurança ou latência.

Em termos de princípios SOLID, ambos promovem:
- Princípio Aberto/Fechado (OCP): sistemas podem ser estendidos sem modificar código existente
- Princípio da Responsabilidade Única (SRP): cada decorator ou proxy gerencia uma preocupação específica

2. Padrão Decorator – Estrutura e Funcionamento

O Decorator é composto por quatro elementos principais:

  1. Componente abstrato: interface que define as operações comuns
  2. ConcreteComponent: implementação base do componente
  3. Decorator abstrato: mantém referência ao componente e delega chamadas
  4. ConcreteDecorators: estendem funcionalidades via composição recursiva
// Interface do componente
public interface IDataProcessor {
    string Process(string data);
}

// Implementação base
public class BaseProcessor : IDataProcessor {
    public string Process(string data) {
        return data.ToUpper();
    }
}

// Decorator abstrato
public abstract class DataProcessorDecorator : IDataProcessor {
    protected IDataProcessor _processor;

    public DataProcessorDecorator(IDataProcessor processor) {
        _processor = processor;
    }

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

// Decorators concretos
public class ValidationDecorator : DataProcessorDecorator {
    public ValidationDecorator(IDataProcessor processor) : base(processor) {}

    public override string Process(string data) {
        if (string.IsNullOrEmpty(data))
            throw new ArgumentException("Dados inválidos");
        return base.Process(data);
    }
}

public class LoggingDecorator : DataProcessorDecorator {
    public LoggingDecorator(IDataProcessor processor) : base(processor) {}

    public override string Process(string data) {
        Console.WriteLine($"Processando: {data}");
        var result = base.Process(data);
        Console.WriteLine($"Resultado: {result}");
        return result;
    }
}

3. Aplicações Arquiteturais do Decorator

O Decorator é ideal para adicionar camadas de funcionalidade sem poluir o núcleo do sistema. Exemplos arquiteturais incluem:

  • Pipeline de processamento de dados: validação → formatação → compressão → persistência
  • Sistemas de cache: adicionar cache em memória para consultas frequentes
  • Logging e auditoria: registrar chamadas sem modificar a lógica de negócio
  • Criptografia: adicionar camadas de segurança em transmissão de dados
// Exemplo de pipeline com empilhamento
IDataProcessor pipeline = new BaseProcessor();
pipeline = new ValidationDecorator(pipeline);
pipeline = new LoggingDecorator(pipeline);
pipeline = new CompressionDecorator(pipeline);

var result = pipeline.Process("dados importantes");

A vantagem arquitetural é a flexibilidade em tempo de execução, evitando a explosão de subclasses que ocorreria com herança tradicional.

4. Padrão Proxy – Estrutura e Funcionamento

O Proxy compartilha uma interface comum (Subject) com o RealSubject, mas controla o acesso a ele. Existem três tipos principais:

  1. Proxy Virtual: lazy loading de recursos pesados
  2. Proxy de Proteção: controle de acesso e segurança
  3. Proxy Remoto: abstração de comunicação em rede
// Interface comum
public interface IImage {
    void Display();
}

// Objeto real (pesado)
public class RealImage : IImage {
    private string _filename;

    public RealImage(string filename) {
        _filename = filename;
        LoadFromDisk();
    }

    private void LoadFromDisk() {
        Console.WriteLine($"Carregando imagem {_filename}");
    }

    public void Display() {
        Console.WriteLine($"Exibindo {_filename}");
    }
}

// Proxy Virtual com lazy loading
public class ImageProxy : IImage {
    private RealImage _realImage;
    private string _filename;

    public ImageProxy(string filename) {
        _filename = filename;
    }

    public void Display() {
        if (_realImage == null) {
            _realImage = new RealImage(_filename);
        }
        _realImage.Display();
    }
}

// Proxy de Proteção
public class SecureImageProxy : IImage {
    private RealImage _realImage;
    private string _filename;
    private string _userRole;

    public SecureImageProxy(string filename, string userRole) {
        _filename = filename;
        _userRole = userRole;
    }

    public void Display() {
        if (_userRole != "Admin") {
            throw new UnauthorizedAccessException("Acesso negado");
        }
        if (_realImage == null) {
            _realImage = new RealImage(_filename);
        }
        _realImage.Display();
    }
}

5. Aplicações Arquiteturais do Proxy

O Proxy resolve problemas arquiteturais específicos:

  • Lazy loading: adiar carregamento de imagens, conexões de banco ou arquivos grandes
  • Controle de acesso: autenticação/autorização em APIs e microsserviços
  • Proxy remoto: comunicação transparente via gRPC, RMI ou WebSockets
  • Cache distribuído: armazenar resultados de consultas frequentes
// Exemplo de proxy remoto em microsserviços
public class RemoteServiceProxy : IService {
    private HttpClient _client;
    private string _baseUrl;

    public async Task<Data> GetDataAsync(int id) {
        var response = await _client.GetAsync($"{_baseUrl}/api/data/{id}");
        return await response.Content.ReadAsAsync<Data>();
    }
}

6. Comparação entre Decorator e Proxy

Aspecto Decorator Proxy
Intenção Adicionar responsabilidades Controlar acesso
Composição Empilhamento recursivo Referência única
Ciclo de vida Gerencia funcionalidades Gerencia o objeto real
Transparência Total para o cliente Pode adicionar lógica de segurança
Exemplo típico Logging, cache, compressão Lazy loading, autenticação

A semelhança estrutural é que ambos implementam a mesma interface do alvo, mas a diferença de intenção é crucial: o Decorator modifica comportamento interno, enquanto o Proxy gerencia o contexto externo.

7. Boas Práticas e Armadilhas Comuns

Armadilhas do Decorator:
- Complexidade excessiva de empilhamento (mais de 5 camadas)
- Dependência da ordem de composição
- Dificuldade de debug com muitas camadas

Armadilhas do Proxy:
- Vazamento da identidade do objeto real (não expor referências internas)
- Overhead desnecessário se o objeto real for leve
- Problemas de concorrência em proxies compartilhados

Boas práticas:
- Manter decorators pequenos e focados em uma única responsabilidade
- Usar injeção de dependência para gerenciar a composição
- Testar cada camada isoladamente com mocks

8. Considerações Finais e Integração na Série

O Decorator e o Proxy são ferramentas poderosas na arquitetura de software, cada um com seu propósito específico. Enquanto o Decorator adiciona funcionalidades de forma incremental, o Proxy gerencia o acesso e o ciclo de vida.

Em um cenário unificado, podemos combiná-los:

// Serviço com cache (proxy) e logging (decorator)
IService service = new LoggingDecorator(
    new CacheProxy(
        new RealService()
    )
);

service.Execute(); // Cache controla acesso ao real service; logging registra chamadas

A escolha entre eles depende do propósito arquitetural: use Decorator quando quiser adicionar comportamento, e Proxy quando precisar controlar acesso ou gerenciar recursos.

Referências