Bridge
Introdução
Seção intitulada “Introdução”O Bridge é um padrão de projeto estrutural que separa uma solução em duas partes independentes: a abstração e a implementação. A ideia central é permitir que essas duas dimensões evoluam separadamente, sem provocar uma explosão no número de subclasses.
Esse padrão aparece quando um problema possui duas variações independentes ao mesmo tempo. Por exemplo: tipo de controle e tipo de dispositivo, tipo de mensagem e canal de envio, ou tipo de relatório e mecanismo de renderização. O Bridge existe para evitar hierarquias de classes grandes, rígidas e difíceis de manter.
Problema
Seção intitulada “Problema”Imagine um sistema de notificações que precisa variar em duas dimensões:
- tipo de notificação:
NotificacaoSimples,NotificacaoUrgente - canal de envio:
Email,SMS,Push
Se a equipe usar apenas herança, logo surgem classes como:
NotificacaoSimplesEmailNotificacaoSimplesSmsNotificacaoUrgenteEmailNotificacaoUrgentePush
Com o crescimento do sistema, os problemas ficam claros:
- o número de classes cresce rápido demais
- cada nova dimensão multiplica combinações existentes
- manutenção e testes ficam mais caros
- pequenas mudanças impactam várias subclasses
- o código fica menos flexível para novas extensões
Esse cenário é conhecido como explosão combinatória de subclasses.
Solução
Seção intitulada “Solução”O Bridge resolve esse problema trocando herança excessiva por composição.
- Separe a parte de alto nível em uma hierarquia de abstração.
- Separe a parte variável de baixo nível em uma hierarquia de implementação.
- Faça a abstração manter uma referência para a implementação.
- Delegue parte do trabalho para esse objeto de implementação.
- Permita que cada lado evolua de forma independente.
Na prática, o Bridge cria uma “ponte” entre duas hierarquias. Assim, não precisamos criar uma classe para cada combinação possível.
Analogia
Seção intitulada “Analogia”Pense em um controle remoto universal. O controle é uma abstração de alto nível: ligar, desligar, aumentar volume. Já a TV, o rádio ou o projetor são implementações concretas desses comandos.
O mesmo modelo de controle pode funcionar com dispositivos diferentes, desde que eles sigam um contrato comum. Do outro lado, um novo dispositivo pode ser adicionado sem exigir a criação de um novo controle para cada caso.
Aplicabilidade
Seção intitulada “Aplicabilidade”Use Bridge quando:
- há duas dimensões independentes de variação no problema
- a hierarquia de classes está crescendo por combinações
- você quer trocar implementações em tempo de execução
- deseja separar lógica de alto nível de detalhes específicos de plataforma
- precisa reduzir acoplamento entre abstração e implementação
Exemplos reais em software moderno:
- interfaces gráficas que variam por plataforma
- notificações com tipos diferentes e múltiplos canais
- relatórios com formatos distintos e diferentes renderizadores
- aplicações multiplataforma com front-end e APIs específicas
- integrações em que o domínio precisa variar independentemente da infraestrutura
Anti-padrão / Mau uso
Seção intitulada “Anti-padrão / Mau uso”Bridge pode ser mal utilizado quando a equipe o aplica sem haver duas dimensões reais e independentes. Os erros mais comuns são:
- criar abstração e implementação quando o problema é simples demais
- confundir Bridge com Strategy ou Adapter sem entender o objetivo
- introduzir camadas extras sem necessidade prática
- usar Bridge apenas porque há composição, sem existir separação conceitual clara
As consequências costumam ser aumento de complexidade, excesso de classes e um design mais difícil de explicar do que o problema original.
Bridge funciona bem quando há independência real entre os dois lados. Se essa independência não existir, o padrão pode virar apenas burocracia arquitetural.
Como implementar
Seção intitulada “Como implementar”Uma forma incremental de implementar Bridge é:
- Identifique as duas dimensões ortogonais do problema.
- Defina a abstração principal que o cliente utilizará.
- Defina uma interface para as implementações concretas.
- Faça a abstração receber uma implementação via construtor.
- Delegue operações para a implementação sempre que fizer sentido.
- Crie abstrações refinadas e implementações concretas conforme a necessidade.
Antes de começar a codificar, vale perguntar: essas duas variações realmente precisam evoluir de forma independente? Se a resposta for sim, Bridge é um forte candidato.
Exemplo em código
Seção intitulada “Exemplo em código”No exemplo abaixo, um sistema de notificações separa o tipo da notificação da forma de envio.
interface CanalEnvio { void enviarMensagem(String titulo, String conteudo);}
class EmailCanal implements CanalEnvio { @Override public void enviarMensagem(String titulo, String conteudo) { System.out.println("Email: " + titulo + " - " + conteudo); }}
class SmsCanal implements CanalEnvio { @Override public void enviarMensagem(String titulo, String conteudo) { System.out.println("SMS: " + titulo + " - " + conteudo); }}
abstract class Notificacao { protected CanalEnvio canal;
public Notificacao(CanalEnvio canal) { this.canal = canal; }
public abstract void enviar(String titulo, String conteudo);}
class NotificacaoSimples extends Notificacao { public NotificacaoSimples(CanalEnvio canal) { super(canal); }
@Override public void enviar(String titulo, String conteudo) { canal.enviarMensagem(titulo, conteudo); }}Nesse código, Notificacao representa a abstração e CanalEnvio representa a implementação. A vantagem é que novas notificações ou novos canais podem ser adicionados separadamente.
Sem o Bridge, o sistema tenderia a gerar várias classes combinando tipo de notificação com canal de entrega.
- evita explosão de subclasses
- separa responsabilidades com mais clareza
- permite evoluir abstração e implementação separadamente
- facilita troca de implementação em tempo de execução
- reduz acoplamento entre domínio e plataforma
Contras
Seção intitulada “Contras”- aumenta a quantidade de classes e interfaces
- pode parecer excessivo em sistemas pequenos
- exige boa modelagem para separar corretamente as dimensões
- pode dificultar o entendimento inicial para iniciantes
Relações com outros padrões/conceitos
Seção intitulada “Relações com outros padrões/conceitos”O Bridge se relaciona com outros padrões importantes:
- Adapter: Adapter conecta interfaces incompatíveis já existentes; Bridge é planejado para separar duas dimensões de evolução
- Strategy: Strategy troca algoritmos; Bridge separa abstração e implementação estruturalmente
- Abstract Factory: pode ser usado junto com Bridge para criar famílias coerentes de implementações
- Decorator: Decorator adiciona comportamento mantendo a mesma estrutura básica; Bridge reorganiza a estrutura em duas hierarquias
Essas comparações ajudam a entender que o Bridge é mais sobre arquitetura e variação independente do que sobre simples substituição de comportamento.
Implementações alternativas (quando aplicável)
Seção intitulada “Implementações alternativas (quando aplicável)”As formas mais comuns de aplicar Bridge são:
- abstração e implementação ligadas por composição
- abstrações refinadas para diferentes fluxos de alto nível
- implementações concretas para plataformas, canais ou motores diferentes
- uso combinado com injeção de dependência para trocar implementações em runtime
Em Java, a implementação mais comum usa interfaces para o lado da implementação e classes abstratas ou concretas para o lado da abstração.
Exemplo completo
Seção intitulada “Exemplo completo”Agora veja um caso mais realista em um sistema acadêmico. A universidade precisa enviar avisos aos alunos por diferentes canais, mas o tipo de notificação também varia.
interface CanalNotificacao { void enviar(String assunto, String mensagem);}
class EmailNotificacao implements CanalNotificacao { @Override public void enviar(String assunto, String mensagem) { System.out.println("[EMAIL] " + assunto + ": " + mensagem); }}
class PushNotificacao implements CanalNotificacao { @Override public void enviar(String assunto, String mensagem) { System.out.println("[PUSH] " + assunto + ": " + mensagem); }}
abstract class AvisoAcademico { protected CanalNotificacao canal;
public AvisoAcademico(CanalNotificacao canal) { this.canal = canal; }
public abstract void disparar();}
class AvisoSimples extends AvisoAcademico { public AvisoSimples(CanalNotificacao canal) { super(canal); }
@Override public void disparar() { canal.enviar("Lembrete", "A atividade fecha hoje às 23h59."); }}
class AvisoUrgente extends AvisoAcademico { public AvisoUrgente(CanalNotificacao canal) { super(canal); }
@Override public void disparar() { canal.enviar("Urgente", "A prova foi reagendada para amanhã."); }}
public class UniversidadeApp { public static void main(String[] args) { AvisoAcademico aviso1 = new AvisoSimples(new EmailNotificacao()); AvisoAcademico aviso2 = new AvisoUrgente(new PushNotificacao());
aviso1.disparar(); aviso2.disparar(); }}Nesse exemplo, AvisoAcademico representa a abstração e CanalNotificacao representa a implementação. Os tipos de aviso podem crescer sem depender dos canais, e os canais podem crescer sem exigir novas subclasses para todas as combinações.
Isso torna o sistema mais modular, reduz repetição e facilita a manutenção em cenários com múltiplas formas de variação.