El patrón Bridge propone desacoplar una abstracción de su implementación para que ambas puedan evolucionar de forma independiente. El objetivo es evitar jerarquías de herencia rígidas donde cada combinación de abstracción e implementación demande una subclase específica.
Al introducir un puente entre ambas dimensiones, el patrón permite crear familias de objetos combinables: una jerarquía de abstracciones (interfaces orientadas al cliente) delega su comportamiento real a otra jerarquía de implementadores intercambiables.
Los diseños orientados a objetos suelen recurrir a la herencia para especializar funcionalidades. Sin embargo, cuando existen dos ejes de variación independientes —por ejemplo, tipos de controles gráficos y plataformas de renderizado—, la combinación de ambos produce una proliferación de clases: BotonWindows
, BotonLinux
, ListaWindows
, ListaLinux
, etc.
Esta explosión de subclases complica el mantenimiento, bloquea la extensión y obliga a modificar código existente ante cada nueva implementación. Bridge evita el problema separando la jerarquía de abstracciones de la jerarquía de implementaciones, estableciendo una relación de composición entre ambas.
La intención del patrón es desacoplar una abstracción de su implementación de modo que puedan variar independientemente. Este enfoque es pertinente cuando:
La motivación se resume en construir sistemas con combinaciones flexibles de responsabilidades sin generar jerarquías de herencia que exploten en complejidad.
Bridge define los siguientes elementos:
La abstracción delega cada operación en el Implementor. De esta forma es posible combinar cualquier refinamiento con cualquier implementación sin crear subclases adicionales.
Imaginemos una librería de controles de interfaz que debe funcionar sobre distintas plataformas de renderizado. El código siguiente muestra cómo separar ambas jerarquías:
public interface Renderizador {
void dibujarBoton(String etiqueta);
void dibujarVentana(String titulo);
}
public class RenderizadorWindows implements Renderizador {
@Override
public void dibujarBoton(String etiqueta) {
System.out.println("Botón Windows: [" + etiqueta + "]");
}
@Override
public void dibujarVentana(String titulo) {
System.out.println("Ventana Windows: <" + titulo + ">");
}
}
public class RenderizadorLinux implements Renderizador {
@Override
public void dibujarBoton(String etiqueta) {
System.out.println("Botón Linux: {" + etiqueta + "}");
}
@Override
public void dibujarVentana(String titulo) {
System.out.println("Ventana Linux: [" + titulo + "]");
}
}
public abstract class Control {
protected final Renderizador renderizador;
protected Control(Renderizador renderizador) {
this.renderizador = renderizador;
}
public abstract void dibujar();
}
public class Boton extends Control {
private final String etiqueta;
public Boton(String etiqueta, Renderizador renderizador) {
super(renderizador);
this.etiqueta = etiqueta;
}
@Override
public void dibujar() {
renderizador.dibujarBoton(etiqueta);
}
}
public class Ventana extends Control {
private final String titulo;
public Ventana(String titulo, Renderizador renderizador) {
super(renderizador);
this.titulo = titulo;
}
@Override
public void dibujar() {
renderizador.dibujarVentana(titulo);
}
}
public class DemoBridge {
public static void main(String[] args) {
Control botonWindows = new Boton("Aceptar", new RenderizadorWindows());
Control botonLinux = new Boton("Aceptar", new RenderizadorLinux());
Control ventanaWindows = new Ventana("Panel principal", new RenderizadorWindows());
botonWindows.dibujar();
botonLinux.dibujar();
ventanaWindows.dibujar();
}
}
Las clases Boton
y Ventana
pertenecen a la jerarquía de abstracciones, mientras que RenderizadorWindows
y RenderizadorLinux
conforman la jerarquía de implementaciones. Nuevos controles o plataformas se agregan sin modificarse entre sí.
Consideremos ahora un sistema de notificaciones que debe soportar diferentes canales (correo, sms, mensajería instantánea) y distintos tipos de mensaje (alertas, recordatorios, informes). Bridge ayuda a combinar ambas jerarquías:
public interface CanalNotificacion {
void enviar(String destinatario, String titulo, String cuerpo);
}
public class CanalEmail implements CanalNotificacion {
@Override
public void enviar(String destinatario, String titulo, String cuerpo) {
System.out.println("Enviando email a " + destinatario + ": " + titulo);
}
}
public class CanalSms implements CanalNotificacion {
@Override
public void enviar(String destinatario, String titulo, String cuerpo) {
System.out.println("SMS para " + destinatario + ": " + cuerpo);
}
}
public abstract class Notificacion {
protected final CanalNotificacion canal;
protected Notificacion(CanalNotificacion canal) {
this.canal = canal;
}
public abstract void enviar(String destinatario);
}
public class NotificacionAlerta extends Notificacion {
private final String mensaje;
public NotificacionAlerta(String mensaje, CanalNotificacion canal) {
super(canal);
this.mensaje = mensaje;
}
@Override
public void enviar(String destinatario) {
canal.enviar(destinatario, "Alerta crítica", mensaje);
}
}
public class NotificacionRecordatorio extends Notificacion {
private final String asunto;
private final String cuerpo;
public NotificacionRecordatorio(String asunto, String cuerpo, CanalNotificacion canal) {
super(canal);
this.asunto = asunto;
this.cuerpo = cuerpo;
}
@Override
public void enviar(String destinatario) {
canal.enviar(destinatario, asunto, cuerpo);
}
}
El método cliente puede crear cualquier combinación de abstracciones (NotificacionAlerta
, NotificacionRecordatorio
) con implementaciones (CanalEmail
, CanalSms
). Si aparece un nuevo canal —por ejemplo, notificaciones push— se añade una implementación sin tocar las clases de la jerarquía de abstracción.
Bridge admite variantes según cómo se administre la referencia al Implementor:
setImplementor
para alternar implementaciones en tiempo de ejecución.En algunos casos se combina con Abstract Factory para crear implementaciones coherentes en familia (todas las variantes de una plataforma), preservando la independencia de ambos ejes de extendibilidad.
Un error común es aplicar Bridge cuando no existe un motivo claro para desacoplar. Si la jerarquía es estable ya se cuenta con una implementación única que no cambiará, el patrón introduce complejidad innecesaria.
También es problemático exponer demasiados métodos en la interfaz Implementor. Si la abstracción delega operaciones excesivamente granulares, se pierde encapsulamiento y aparece un acoplamiento fuerte entre ambos lados del puente.
Bridge se complementa con Adapter cuando se necesita adaptar una implementación heredada a la interfaz esperada. Decorator puede envolver implementaciones concretas para agregar responsabilidades sin alterar el puente. Si se requiere construir jerarquías complejas de implementadores compatibles, Abstract Factory resulta una pareja frecuente.
Evalúa el uso de Bridge cuando la herencia genera combinaciones explosivas o cuando se desea publicar una API estable sobre implementaciones que evolucionan a ritmos distintos. Si la variabilidad solo se presenta en un eje, bastará una jerarquía tradicional.