Pular para o conteúdo

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.

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:

  • NotificacaoSimplesEmail
  • NotificacaoSimplesSms
  • NotificacaoUrgenteEmail
  • NotificacaoUrgentePush

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.

O Bridge resolve esse problema trocando herança excessiva por composição.

  1. Separe a parte de alto nível em uma hierarquia de abstração.
  2. Separe a parte variável de baixo nível em uma hierarquia de implementação.
  3. Faça a abstração manter uma referência para a implementação.
  4. Delegue parte do trabalho para esse objeto de implementação.
  5. 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.

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.

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

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.

Uma forma incremental de implementar Bridge é:

  1. Identifique as duas dimensões ortogonais do problema.
  2. Defina a abstração principal que o cliente utilizará.
  3. Defina uma interface para as implementações concretas.
  4. Faça a abstração receber uma implementação via construtor.
  5. Delegue operações para a implementação sempre que fizer sentido.
  6. 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.

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
  • 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

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.

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.

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.