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:
NotificacaoNormalPorEmailNotificacaoNormalPorSmsNotificacaoUrgentePorEmailNotificacaoUrgentePorSms
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:
-
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
Notificacaocom referência aCanalEnvio. -
RefinedAbstraction (Abstração Refinada): estende a
Abstração, adicionando comportamento específico de cada variante.
Ex.:
NotificacaoNormal,NotificacaoUrgente. -
Implementor (interface): define o contrato do eixo
de implementação — tipicamente operações de baixo nível. Ex.: interface
CanalEnviocomenviar(destinatario, mensagem). -
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
<?php
// ── Implementador — eixo do canal de envio ────────────────────
interface CanalEnvio
{
public function enviar(string $destinatario, string $mensagem): void;
}
// ── ConcreteImplementors ──────────────────────────────────────
class EmailSender implements CanalEnvio
{
public function enviar(string $destinatario, string $mensagem): void
{
echo "[EMAIL para {$destinatario}] {$mensagem}" . PHP_EOL;
}
}
class SmsSender implements CanalEnvio
{
public function enviar(string $destinatario, string $mensagem): void
{
echo "[SMS para {$destinatario}] {$mensagem}" . PHP_EOL;
}
}
// Adicionar WhatsAppSender não afeta nenhuma classe de Notificacao:
class WhatsAppSender implements CanalEnvio
{
public function enviar(string $destinatario, string $mensagem): void
{
echo "[WA para {$destinatario}] {$mensagem}" . PHP_EOL;
}
}
// ── Abstração — eixo do tipo de notificação ───────────────────
abstract class Notificacao
{
// A "ponte": referência ao implementador — injetada no construtor.
public function __construct(
protected readonly CanalEnvio $canal
) {}
abstract public function notificar(string $destinatario, string $conteudo): void;
}
// ── Abstrações refinadas ──────────────────────────────────────
class NotificacaoNormal extends Notificacao
{
public function notificar(string $destinatario, string $conteudo): void
{
$this->canal->enviar($destinatario, $conteudo);
}
}
class NotificacaoUrgente extends Notificacao
{
public function notificar(string $destinatario, string $conteudo): void
{
$this->canal->enviar($destinatario, "[URGENTE] {$conteudo}");
}
}
// Adicionar NotificacaoAgendada não afeta nenhuma classe de CanalEnvio:
class NotificacaoAgendada extends Notificacao
{
public function __construct(
CanalEnvio $canal,
private readonly string $agendadaPara
) {
parent::__construct($canal);
}
public function notificar(string $destinatario, string $conteudo): void
{
$this->canal->enviar(
$destinatario,
"[AGENDADA {$this->agendadaPara}] {$conteudo}"
);
}
}
// ── Uso — combinações livres sem novas subclasses ─────────────
$email = new EmailSender();
$sms = new SmsSender();
$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)
<?php
// ── Implementador — eixo do renderizador ──────────────────────
interface Renderizador
{
public function renderizarForma(string $tipo, string $params): void;
}
class RenderizadorVetorial implements Renderizador
{
public function renderizarForma(string $tipo, string $params): void
{
echo "[SVG] {$tipo}({$params})" . PHP_EOL;
}
}
class RenderizadorRaster implements Renderizador
{
public function renderizarForma(string $tipo, string $params): void
{
echo "[Canvas] {$tipo}({$params})" . PHP_EOL;
}
}
// ── Abstração — eixo da forma geométrica ──────────────────────
abstract class Forma
{
public function __construct(
protected Renderizador $renderizador
) {}
abstract public function desenhar(): void;
// O implementador pode ser trocado em runtime:
public function trocarRenderizador(Renderizador $renderizador): void
{
$this->renderizador = $renderizador;
}
}
// ── Abstrações refinadas ──────────────────────────────────────
class Circulo extends Forma
{
public function __construct(
Renderizador $renderizador,
private readonly int $raio
) {
parent::__construct($renderizador);
}
public function desenhar(): void
{
$this->renderizador->renderizarForma('circulo', "r={$this->raio}");
}
}
class Retangulo extends Forma
{
public function __construct(
Renderizador $renderizador,
private readonly int $largura,
private readonly int $altura
) {
parent::__construct($renderizador);
}
public function desenhar(): void
{
$this->renderizador->renderizarForma(
'retangulo',
"w={$this->largura} h={$this->altura}"
);
}
}
// ── Uso ───────────────────────────────────────────────────────
$vetorial = new RenderizadorVetorial();
$raster = new RenderizadorRaster();
$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.