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.
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:
- 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.
- Protection Proxy: Verifica permissões e papéis de segurança antes de repassar a execução para a classe de negócio.
- Remote Proxy: Representa um objeto residente em outro processo ou máquina, serializando requisições (como clientes gRPC gerados).
- 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.