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.
Consideremos una jerarquía de controles gráficos que puede renderizarse usando distintas APIs (por ejemplo, GDI+ en Windows o SkiaSharp multiplataforma). La abstracción representa el control, mientras que la implementación concreta encapsula el mecanismo de dibujo:
using System;
public interface IRenderizador
{
void DibujarRectangulo(int x, int y, int ancho, int alto);
void DibujarTexto(string texto, int x, int y);
}
public class RenderizadorWindows : IRenderizador
{
public void DibujarRectangulo(int x, int y, int ancho, int alto)
{
Console.WriteLine($"[Windows] Rectangulo en ({x},{y}) {ancho}x{alto}");
}
public void DibujarTexto(string texto, int x, int y)
{
Console.WriteLine($"[Windows] Texto en ({x},{y}): {texto}");
}
}
public class RenderizadorLinux : IRenderizador
{
public void DibujarRectangulo(int x, int y, int ancho, int alto)
{
Console.WriteLine($"[Linux] Rectangulo en ({x},{y}) {ancho}x{alto}");
}
public void DibujarTexto(string texto, int x, int y)
{
Console.WriteLine($"[Linux] Texto en ({x},{y}): {texto}");
}
}
public abstract class Control
{
protected Control(IRenderizador renderizador)
{
Renderizador = renderizador;
}
protected IRenderizador Renderizador { get; }
public abstract void Dibujar();
}
public class Boton : Control
{
private readonly string _texto;
private readonly int _x;
private readonly int _y;
public Boton(IRenderizador renderizador, string texto, int x, int y) : base(renderizador)
{
_texto = texto;
_x = x;
_y = y;
}
public override void Dibujar()
{
Renderizador.DibujarRectangulo(_x, _y, 120, 30);
Renderizador.DibujarTexto(_texto, _x + 10, _y + 18);
}
}
public class Lista : Control
{
private readonly string[] _items;
private readonly int _x;
private readonly int _y;
public Lista(IRenderizador renderizador, string[] items, int x, int y) : base(renderizador)
{
_items = items;
_x = x;
_y = y;
}
public override void Dibujar()
{
Renderizador.DibujarRectangulo(_x, _y, 160, 100);
for (int i = 0; i < _items.Length; i++)
{
Renderizador.DibujarTexto(_items[i], _x + 10, _y + 20 + i * 20);
}
}
}
public static class DemoBridge
{
public static void Main()
{
Control botonWindows = new Boton(new RenderizadorWindows(), "Guardar", 10, 10);
Control listaLinux = new Lista(new RenderizadorLinux(), new[] { "Item 1", "Item 2" }, 20, 50);
botonWindows.Dibujar();
listaLinux.Dibujar();
}
}
La abstracción Control
delega la lógica de dibujo a IRenderizador
. Cada jerarquía evoluciona de manera independiente y el cliente combina implementaciones en tiempo de ejecución.
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:
using System;
public interface ICanalNotificacion
{
void Enviar(string destinatario, string titulo, string cuerpo);
}
public class CanalEmail : ICanalNotificacion
{
public void Enviar(string destinatario, string titulo, string cuerpo)
{
Console.WriteLine($"Enviando email a {destinatario}: {titulo}");
}
}
public class CanalSms : ICanalNotificacion
{
public void Enviar(string destinatario, string titulo, string cuerpo)
{
Console.WriteLine($"SMS para {destinatario}: {cuerpo}");
}
}
public abstract class Notificacion
{
protected Notificacion(ICanalNotificacion canal)
{
Canal = canal;
}
protected ICanalNotificacion Canal { get; }
public abstract void Enviar(string destinatario);
}
public class NotificacionAlerta : Notificacion
{
private readonly string _mensaje;
public NotificacionAlerta(string mensaje, ICanalNotificacion canal) : base(canal)
{
_mensaje = mensaje;
}
public override void Enviar(string destinatario)
{
Canal.Enviar(destinatario, "Alerta critica", _mensaje);
}
}
public class NotificacionRecordatorio : Notificacion
{
private readonly string _asunto;
private readonly string _cuerpo;
public NotificacionRecordatorio(string asunto, string cuerpo, ICanalNotificacion canal) : base(canal)
{
_asunto = asunto;
_cuerpo = cuerpo;
}
public override void Enviar(string destinatario)
{
Canal.Enviar(destinatario, _asunto, _cuerpo);
}
}
public static class DemoNotificaciones
{
public static void Main()
{
Notificacion alertaSms = new NotificacionAlerta("Servidor caido", new CanalSms());
Notificacion recordatorioEmail = new NotificacionRecordatorio(
"Recordatorio",
"Reunion de seguimiento a las 10:00",
new CanalEmail());
alertaSms.Enviar("+543511234567");
recordatorioEmail.Enviar("usuario@example.com");
}
}
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.