Software · Design Patterns

Padrões Estruturais Avançados: Bridge, Composite, Proxy e Flyweight

Como completar o repertório estrutural do GoF: separação ortogonal de dimensões com Bridge, hierarquias árvore com Composite, intermediação controlada com Proxy e otimização de memória com Flyweight.

Diagrama de padrões estruturais GoF ilustrando pontes de abstração Bridge, estruturas em árvore Composite, intermediários Proxy e nós compartilhados Flyweight
Neste artigo

Quando sistemas orientados a objetos crescem, herança rígida frequentemente resulta em proliferação incontrolável de classes. Se uma aplicação precisa suportar três tipos de relatórios operando sobre quatro formatos de exportação diferentes, uma abordagem puramente baseada em subclasses exigiria doze classes distintas para cobrir todas as combinações.

Enquanto padrões como Adapter, Decorator e Facade resolvem incompatibilidades e empilhamento de responsabilidades, os demais membros do catálogo estrutural do Gang of Four — Bridge, Composite, Proxy e Flyweight — lidam com desafios mais sutis: divisão de hierarquias ortogonais, tratamento uniforme de árvores de objetos, controle de acesso a recursos caros e redução drástica da pegada de memória.

flowchart TD
    subgraph BridgeEx["Bridge: Abstração desacoplada de Implementação"]
        Report["Relatório (Abstração)"] -->|contém referência| IExport["IExportFormatter (Implementador)"]
        PdfFormat["PdfFormatter"] -.->|implementa| IExport
        ExcelFormat["ExcelFormatter"] -.->|implementa| IExport
    end

    subgraph CompositeEx["Composite: Hierarquia Todo-Parte"]
        RootFolder["Pasta Raiz (Composite)"] --> SubFolder["Subpasta (Composite)"]
        RootFolder --> Doc1["Documento A (Folha)"]
        SubFolder --> Doc2["Documento B (Folha)"]
    end

Comparativo Estrutural

Cada um desses quatro padrões aborda uma necessidade arquitetural específica com trade-offs bem delimitados:

Padrão Problema Central Mecanismo de Solução Aplicação Real em Produção
Bridge Duas dimensões independentes variam simultaneamente Separação em duas hierarquias ligadas por composição Drivers de banco de dados, renderizadores de interface multiplataforma
Composite Clientes precisam tratar nós individuais e grupos de forma idêntica Interface comum para nós folha (leaves) e nós compostos (composites) Árvores DOM de HTML, sistemas de arquivos, menus e estruturas organizacionais
Proxy Acesso direto a um objeto é caro, inseguro ou remoto Objeto substituto com a mesma interface intermediando o acesso Lazy loading em ORMs, proxies reversos, controle de permissão e caching
Flyweight Milhares de objetos semelhantes causam estouro de memória (OutOfMemory) Separação entre estado intrínseco (compartilhado) e extrínseco (passado por parâmetro) Editores de texto (glifos de fontes), motores de jogos e string interning

1. Bridge: Evitando a Explosão Combinatória

O padrão Bridge desacopla uma abstração de sua implementação para que ambas possam variar de forma totalmente independente.

Considere um sistema de notificações que precisa enviar alertas com diferentes urgências (Normal, Crítica) através de múltiplos canais (Email, SMS, Slack). Em vez de criar classes como CriticaEmailNotification, CriticaSmsNotification e NormalSlackNotification, o padrão divide o problema em dois eixos:

// Eixo 1: A Implementação (Canais)
public interface IMessageChannel 
{
    Task SendMessageAsync(string target, string message);
}

public class EmailChannel : IMessageChannel 
{
    public Task SendMessageAsync(string target, string message) =>
        Console.Out.WriteLineAsync($"[EMAIL para {target}]: {message}");
}

// Eixo 2: A Abstração (Tipos de Notificação)
public abstract class Notification 
{
    protected readonly IMessageChannel _channel; // A "Ponte" (Bridge)

    protected Notification(IMessageChannel channel) 
    {
        _channel = channel;
    }

    public abstract Task NotifyAsync(string target, string body);
}

public class CriticalAlertNotification : Notification 
{
    public CriticalAlertNotification(IMessageChannel channel) : base(channel) { }

    public override async Task NotifyAsync(string target, string body) 
    {
        var formatted = $"[URGENTE] ALERTA CRÍTICO: {body.ToUpper()}";
        await _channel.SendMessageAsync(target, formatted);
    }
}

Adicionar um novo canal (como WhatsApp) agora exige apenas uma nova classe implementando IMessageChannel, sem tocar em nenhuma das subclasses de Notification.

2. Composite: O Tratamento Uniforme de Árvores

O Composite permite compor objetos em estruturas em árvore para representar hierarquias todo-parte. A beleza do padrão reside no fato de que o cliente trata elementos primitivos e coleções de elementos usando o mesmo contrato.

public interface IFileSystemItem 
{
    string Name { get; }
    long GetSizeInBytes();
}

// Elemento Folha (Leaf)
public class FileItem : IFileSystemItem 
{
    public string Name { get; }
    private readonly long _size;

    public FileItem(string name, long size) 
    {
        Name = name;
        _size = size;
    }

    public long GetSizeInBytes() => _size;
}

// Elemento Composto (Composite)
public class DirectoryItem : IFileSystemItem 
{
    public string Name { get; }
    private readonly List<IFileSystemItem> _children = new();

    public DirectoryItem(string name) 
    {
        Name = name;
    }

    public void Add(IFileSystemItem item) => _children.Add(item);

    public long GetSizeInBytes() => _children.Sum(child => child.GetSizeInBytes());
}

O código que calcula o tamanho de um diretório não precisa inspecionar tipos usando operadores como is ou as; ele simplesmente invoca GetSizeInBytes(), e a recursão natural da árvore calcula a soma de forma limpa.

3. Proxy: Intermediação Transparente

O Proxy fornece um substituto com a mesma interface de um objeto real para interceptar chamadas. Existem quatro variantes clássicas em engenharia de software:

  1. Virtual Proxy (Lazy Loading): Adia a instanciação de um objeto pesado (como uma imagem ou um documento complexo) até o momento exato em que algum método dele é invocado.
  2. Protection Proxy: Verifica permissões e papéis de segurança antes de repassar a execução para a classe de negócio.
  3. Remote Proxy: Representa um objeto residente em outro processo ou máquina, serializando requisições (como clientes gRPC gerados).
  4. Caching Proxy: Armazena respostas anteriores para evitar processamento ou consultas repetitivas de banco.

4. Flyweight: Otimização de Memória com Estado Compartilhado

Quando uma aplicação instancia centenas de milhares de entidades semelhantes, grande parte dos dados armazenados em cada objeto é idêntica. O Flyweight resolve isso dividindo o estado do objeto em duas partes:

  • Estado Intrínseco: Dados imutáveis e compartilhados entre múltiplos contextos (exemplo: a forma visual e a textura de uma árvore em um mapa 3D).
  • Estado Extrínseco: Dados variáveis e exclusivos de cada instância, passados como parâmetros durante a execução (exemplo: as coordenadas (X, Y, Z) de cada árvore no terreno).
flowchart LR
    Factory["Flyweight Factory (Gerenciador de Modelos)"]
    SharedMesh["Malha 3D 'Pinheiro' (Estado Intrínseco em RAM Única)"]
    
    Tree1["Árvore Instância 1 (Posição: 10, 20)"] --> SharedMesh
    Tree2["Árvore Instância 2 (Posição: 45, 80)"] --> SharedMesh
    Tree3["Árvore Instância 3 (Posição: 90, 15)"] --> SharedMesh

Na plataforma .NET, o mecanismo de String Interning (string.Intern) opera exatamente como um Flyweight: se dez mil strings com o conteúdo "PROCESSADO_SUCESSO" são internadas, o CLR armazena os caracteres na memória uma única vez, fazendo com que todas as referências apontem para o mesmo endereço de memória.

Explorar este tema no Grafo de Conhecimento