Padrão Estrutural (GoF)

Bridge

Separa uma abstração da sua implementação para que ambas possam variar independentemente — usando composição em vez de herança para evitar a explosão combinatória de subclasses.

Intenção

Desacoplar abstração de implementação por meio de composição, de modo que os dois eixos possam variar e evoluir de forma completamente independente. A abstração delega para o implementador em vez de herdar dele — substituindo a hierarquia N×M de subclasses por N + M classes.

Catalogado pelo GoF (1994) como padrão estrutural, o Bridge é a resposta planejada ao problema da explosão combinatória de herança: quando uma classe tem dois (ou mais) eixos de variação independentes, criar uma subclasse para cada combinação escala de forma insustentável. O Bridge resolve isso separando os dois eixos em hierarquias distintas e ligando-os por composição.

Problema

Imagine um sistema de notificações com dois eixos de variação independentes: o tipo (normal, urgente) e o canal de envio (e-mail, SMS). Se você modelar isso com herança pura, precisa de uma subclasse para cada combinação:

  • NotificacaoNormalPorEmail
  • NotificacaoNormalPorSms
  • NotificacaoUrgentePorEmail
  • NotificacaoUrgentePorSms

Com apenas 2 tipos e 2 canais, já são 4 classes. Adicionar um canal WhatsApp e um tipo Agendada eleva para 3×3 = 9 classes. Com 5 tipos e 5 canais: 25 classes. O crescimento é multiplicativo — e qualquer mudança em uma dimensão exige atualizar a outra.

O Bridge elimina a explosão: cada eixo tem a sua própria hierarquia. Tipos e canais evoluem de forma completamente independente. Adicionar WhatsApp é criar uma classe WhatsAppSender — sem tocar em nenhum tipo de notificação.

Bridge vs Adapter: a distinção crítica

Esta é a confusão mais frequente com o Bridge. A diferença está no momento e na intenção:

  • O Adapter é aplicado depois — você tem duas interfaces incompatíveis que existem de forma independente, e o Adapter as reconcilia. É uma solução corretiva para um problema de integração existente.
  • O Bridge é planejado desde o início — você identifica dois eixos de variação independentes no design e os separa intencionalmente para que possam evoluir sem se afetar. É uma decisão de arquitetura preventiva.

Em resumo: Adapter concilia incompatibilidades existentes; Bridge previne o acoplamento entre dimensões de variação desde o design.

Bridge vs Strategy

Ambos usam composição para delegar comportamento, e a estrutura de código pode ser idêntica. A diferença é conceitual:

  • O Strategy é um padrão comportamental com um único eixo de variação: o algoritmo. O contexto escolhe qual Strategy usar e pode trocá-lo em runtime. O foco é na intercambialidade de algoritmos.
  • O Bridge é um padrão estrutural com dois eixos de variação independentes: abstração e implementação. Ambos podem variar e ter hierarquias próprias. O foco é na separação de dimensões estruturais do design.

Solução

O Bridge organiza o código em quatro participantes:

  1. Abstraction (Abstração): define a interface de alto nível e mantém uma referência ao Implementor. Pode ser uma classe abstrata ou concreta. Ex.: classe abstrata Notificacao com referência a CanalEnvio.
  2. RefinedAbstraction (Abstração Refinada): estende a Abstração, adicionando comportamento específico de cada variante. Ex.: NotificacaoNormal, NotificacaoUrgente.
  3. Implementor (interface): define o contrato do eixo de implementação — tipicamente operações de baixo nível. Ex.: interface CanalEnvio com enviar(destinatario, mensagem).
  4. ConcreteImplementor: realiza o contrato do Implementor. Ex.: EmailSender, SmsSender. Novas implementações não afetam nenhuma classe de abstração.

A "ponte" (bridge) é a referência que a Abstração mantém para o Implementor. Como essa referência é do tipo da interface CanalEnvio (não de uma classe concreta), qualquer ConcreteImplementor pode ser injetado — em tempo de construção ou em runtime.

Estrutura

  Eixo da Abstração                     Eixo da Implementação

  Notificacao (abstrata)                «interface» CanalEnvio
  ┌──────────────────────────────┐      ┌──────────────────────────────────┐
  │ # canal: CanalEnvio          │─────▶│ + enviar(dest, msg): void        │
  │ + notificar(dest, msg): void │      └──────────────────────────────────┘
  └──────────────────────────────┘                   ▲
               ▲                            ┌────────┴────────┐
       ┌───────┴────────┐             EmailSender         SmsSender
  NotificacaoNormal  NotificacaoUrgente
  (Abstração Refinada)  (Abstração Refinada)


Sem Bridge — explosão N×M de subclasses:

  2 tipos × 2 canais = 4 classes
  NotificacaoNormalPorEmail
  NotificacaoNormalPorSms
  NotificacaoUrgentePorEmail
  NotificacaoUrgentePorSms
  → com 5 tipos e 5 canais: 25 classes

Com Bridge — N + M classes:

  2 tipos + 2 canais = 4 classes
  NotificacaoNormal, NotificacaoUrgente
  EmailSender, SmsSender
  → com 5 tipos e 5 canais: apenas 10 classes


Fluxo de uma chamada:

  urgente.notificar("ops@empresa.com", "Servidor fora do ar!")
      │
      ▼ (NotificacaoUrgente.notificar)
      │  prefixo "[URGENTE]" aplicado
      │
      ▼ (delega para this.canal.enviar)
  EmailSender.enviar("ops@empresa.com", "[URGENTE] Servidor fora do ar!")
      │
      ▼
  [EMAIL para ops@empresa.com] [URGENTE] Servidor fora do ar!

Exemplos de código

Exemplo 1 — Notificações por tipo e canal

A abstração (Notificacao) e o implementador (CanalEnvio) variam de forma completamente independente. Adicionar um novo canal ou um novo tipo de notificação não requer alterar o outro eixo.

// ── Implementador — eixo do canal de envio ────────────────────
interface CanalEnvio {
  enviar(destinatario: string, mensagem: string): void;
}

// ── ConcreteImplementors ──────────────────────────────────────
class EmailSender implements CanalEnvio {
  enviar(destinatario: string, mensagem: string): void {
    console.log(`[EMAIL para ${destinatario}] ${mensagem}`);
  }
}

class SmsSender implements CanalEnvio {
  enviar(destinatario: string, mensagem: string): void {
    console.log(`[SMS para ${destinatario}] ${mensagem}`);
  }
}

// Adicionar WhatsAppSender não afeta nenhuma classe de Notificacao:
class WhatsAppSender implements CanalEnvio {
  enviar(destinatario: string, mensagem: string): void {
    console.log(`[WA para ${destinatario}] ${mensagem}`);
  }
}

// ── Abstração — eixo do tipo de notificação ───────────────────
abstract class Notificacao {
  // A "ponte": referência ao implementador — injetada no construtor.
  constructor(protected readonly canal: CanalEnvio) {}

  abstract notificar(destinatario: string, conteudo: string): void;
}

// ── Abstrações refinadas ──────────────────────────────────────
class NotificacaoNormal extends Notificacao {
  notificar(destinatario: string, conteudo: string): void {
    this.canal.enviar(destinatario, conteudo);
  }
}

class NotificacaoUrgente extends Notificacao {
  notificar(destinatario: string, conteudo: string): void {
    this.canal.enviar(destinatario, `[URGENTE] ${conteudo}`);
  }
}

// Adicionar NotificacaoAgendada não afeta nenhuma classe de CanalEnvio:
class NotificacaoAgendada extends Notificacao {
  constructor(canal: CanalEnvio, private readonly agendadaPara: string) {
    super(canal);
  }
  notificar(destinatario: string, conteudo: string): void {
    this.canal.enviar(
      destinatario,
      `[AGENDADA ${this.agendadaPara}] ${conteudo}`
    );
  }
}

// ── Uso — combinações livres sem novas subclasses ─────────────
const email = new EmailSender();
const sms   = new SmsSender();
const wa    = new WhatsAppSender();

new NotificacaoNormal(email).notificar(
  "ana@exemplo.com", "Seu pedido foi confirmado."
);
// [EMAIL para ana@exemplo.com] Seu pedido foi confirmado.

new NotificacaoUrgente(email).notificar(
  "ops@empresa.com", "Servidor fora do ar!"
);
// [EMAIL para ops@empresa.com] [URGENTE] Servidor fora do ar!

new NotificacaoUrgente(sms).notificar(
  "+55119999-0000", "Servidor fora do ar!"
);
// [SMS para +55119999-0000] [URGENTE] Servidor fora do ar!

new NotificacaoAgendada(wa, "2026-07-01 09:00").notificar(
  "+55119999-0001", "Lembrete de reunião"
);
// [WA para +55119999-0001] [AGENDADA 2026-07-01 09:00] Lembrete de reunião

Exemplo 2 — Formas geométricas com renderizadores intercambiáveis

O exemplo clássico do GoF: formas (abstração) variam independentemente do renderizador (implementação). Um novo tipo de forma não requer nenhum renderizador novo, e um novo renderizador não requer nenhuma forma nova.

// ── Implementador — eixo do renderizador ──────────────────────
interface Renderizador {
  renderizarForma(tipo: string, params: string): void;
}

class RenderizadorVetorial implements Renderizador {
  renderizarForma(tipo: string, params: string): void {
    console.log(`[SVG]    ${tipo}(${params})`);
  }
}

class RenderizadorRaster implements Renderizador {
  renderizarForma(tipo: string, params: string): void {
    console.log(`[Canvas] ${tipo}(${params})`);
  }
}

// ── Abstração — eixo da forma geométrica ──────────────────────
abstract class Forma {
  constructor(protected renderizador: Renderizador) {}
  abstract desenhar(): void;
  // O implementador pode ser trocado em runtime:
  trocarRenderizador(renderizador: Renderizador): void {
    this.renderizador = renderizador;
  }
}

// ── Abstrações refinadas ──────────────────────────────────────
class Circulo extends Forma {
  constructor(renderizador: Renderizador, private readonly raio: number) {
    super(renderizador);
  }
  desenhar(): void {
    this.renderizador.renderizarForma("circulo", `r=${this.raio}`);
  }
}

class Retangulo extends Forma {
  constructor(
    renderizador: Renderizador,
    private readonly largura: number,
    private readonly altura: number
  ) {
    super(renderizador);
  }
  desenhar(): void {
    this.renderizador.renderizarForma(
      "retangulo", `w=${this.largura} h=${this.altura}`
    );
  }
}

// ── Uso ───────────────────────────────────────────────────────
const vetorial = new RenderizadorVetorial();
const raster   = new RenderizadorRaster();

const c = new Circulo(vetorial, 50);
c.desenhar();
// [SVG]    circulo(r=50)

new Circulo(raster, 50).desenhar();
// [Canvas] circulo(r=50)

new Retangulo(vetorial, 200, 100).desenhar();
// [SVG]    retangulo(w=200 h=100)

new Retangulo(raster, 200, 100).desenhar();
// [Canvas] retangulo(w=200 h=100)

// Troca de renderizador em runtime — sem subclasse nova:
c.trocarRenderizador(raster);
c.desenhar();
// [Canvas] circulo(r=50)

Quando usar

  • Quando você identifica dois eixos independentes de variação: qualquer estrutura onde "tipo de X" e "implementação de Y" variam separadamente e você não quer criar uma subclasse para cada combinação.
  • Quando abstração e implementação devem ser extensíveis de forma independente: equipes diferentes mantêm cada eixo; novas implementações não devem afetar as abstrações e vice-versa.
  • Quando você quer trocar a implementação em runtime: como o implementador é injetado por composição, ele pode ser substituído sem recriar a abstração.
  • Para ocultar detalhes de implementação do código cliente: o cliente só conhece a interface da abstração; os ConcreteImplementors ficam completamente encapsulados.

Quando evitar

  • Quando há apenas um eixo de variação: se só a implementação varia (e não a abstração), um simples Strategy já resolve sem a estrutura adicional do Bridge.
  • Quando o design ainda não está claro: o Bridge exige identificar os dois eixos de variação antecipadamente. Aplicá-lo cedo demais a um design instável gera abstração prematura que dificulta mudanças subsequentes.
  • Quando a hierarquia é pequena e estável: se você tem 2 tipos e 2 implementações que raramente mudam, 4 subclasses diretas podem ser mais simples e legíveis do que a estrutura formal de Bridge.

Prós e contras

Prós

  • Elimina a explosão combinatória de subclasses — N + M classes no lugar de N × M.
  • Princípio Aberto/Fechado: novos tipos de abstração ou implementação podem ser adicionados sem alterar o outro eixo.
  • Princípio da Responsabilidade Única: cada classe tem um único eixo de variação.
  • Permite trocar a implementação em runtime por injeção de dependência.
  • Oculta detalhes de implementação do código cliente — apenas a abstração é exposta.

Contras

  • Adiciona complexidade estrutural — mais classes, mais indireção, maior curva de aprendizado para quem lê o código pela primeira vez.
  • Exige identificar os dois eixos de variação no design inicial — pode ser difícil em sistemas que ainda estão evoluindo.
  • A relação entre abstração e implementação pode ser contraintuitiva para desenvolvedores não familiarizados com o padrão.

Armadilhas comuns

1. Usar Bridge quando Strategy bastaria

Se apenas a implementação varia (e não há uma hierarquia de abstrações), o Bridge é desnecessariamente complexo. A regra prática: se você tem uma única classe de abstração e vários implementadores, use Strategy. Se você tem hierarquias em ambos os lados, use Bridge.

2. Confundir Bridge com Adapter

Atenção: a tentação de usar Bridge como Adapter é frequente. Lembre-se: o Adapter reconcilia duas interfaces incompatíveis que existem de forma independente — é uma solução retroativa. O Bridge é projetado desde o início para separar dois eixos de variação — é uma decisão prospectiva. Se você está "adaptando" uma interface existente, use Adapter. Se está projetando dois eixos independentes, use Bridge.

3. Acoplamento acidental entre os dois eixos

O objetivo do Bridge é que abstração e implementação variem de forma independente. Se a abstração começa a chamar métodos específicos de um ConcreteImplementor (fazendo downcast ou chamando métodos além da interface Implementor), o desacoplamento foi quebrado. O Implementor deve expor apenas o que é necessário para todas as abstrações. Se diferentes abstrações precisam de contratos diferentes do implementador, avalie se Bridge ainda é o padrão correto.

4. Não injetar o implementador — instanciar dentro da abstração

Um erro sutil é a abstração instanciar o implementador concretamente dentro de si mesma (this.canal = new EmailSender()). Isso restaura o acoplamento que o Bridge visa eliminar. O implementador deve sempre ser injetado de fora — via construtor, setter ou Abstract Factory — para que os dois eixos permaneçam independentes e testáveis.

Padrões relacionados

O Bridge é frequentemente comparado a outros padrões que também usam composição para delegar comportamento:

O Adapter é o padrão mais confundido com Bridge: Adapter reconcilia interfaces incompatíveis existentes (solução retroativa); Bridge separa dois eixos independentes desde o design (solução prospectiva). O Strategy também usa composição para delegar, mas tem um único eixo de variação (o algoritmo); Bridge tem dois eixos com hierarquias próprias. O Abstract Factory pode ser combinado com Bridge para criar o par abstração + implementador de forma consistente — a factory instancia a implementação correta e a injeta na abstração. O Proxy também usa composição, mas representa um único objeto e controla o acesso a ele; Bridge separa estruturalmente dois eixos de variação independentes.