15. Bridge (Puente) - Patrón Estructural

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.

15.1 Problema y contexto

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.

15.2 Intención y motivación

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:

  • Se prevé agregar nuevas implementaciones con frecuencia sin alterar la interfaz del cliente.
  • Existe la necesidad de intercambiar la implementación en tiempo de ejecución (por ejemplo, cambiar de proveedor de bases de datos o de API gráfica).
  • Se requiere compartir código que opera sobre la abstracción, sin comprometer la evolución de las clases concretas.

La motivación se resume en construir sistemas con combinaciones flexibles de responsabilidades sin generar jerarquías de herencia que exploten en complejidad.

15.3 Participantes y estructura

Bridge define los siguientes elementos:

  • Abstraction: interfaz o clase abstracta que expone operaciones de alto nivel al cliente. Mantiene una referencia a un Implementor.
  • RefinedAbstraction: extensiones concretas de la abstracción que pueden agregar responsabilidades sin afectar la implementación subyacente.
  • Implementor: interfaz con operaciones primitivas que sirven de base para la abstracción.
  • ConcreteImplementor: implementaciones específicas (drivers, adaptadores, conectores) que definen el comportamiento real.

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.

15.4 Implementación básica en C#

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.

15.5 Caso extendido: notificaciones multiplataforma

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");
    }
}

15.6 Variantes y decisiones de diseño

Bridge admite variantes según cómo se administre la referencia al Implementor:

  • Asignación estática: la referencia se recibe en el constructor y no cambia. Adecuado cuando la combinación se decide al crear el objeto.
  • Asignación dinámica: la abstracción expone un método setImplementor para alternar implementaciones en tiempo de ejecución.
  • Múltiples implementadores: la abstracción puede coordinar varios implementadores (por ejemplo, dibujar en pantalla y registrar en un log) siempre que mantenga el principio de responsabilidad única.

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.

15.7 Beneficios

  • Reduce el número de clases necesarias para soportar combinaciones de abstracciones e implementaciones.
  • Permite agregar nuevas implementaciones sin recompilar ni modificar la jerarquía que interactúa con el cliente.
  • Facilita pruebas porque las implementaciones concretas pueden sustituirse por dobles (mocks, stubs) al compartir la misma interfaz.
  • Promueve la composición sobre la herencia, preservando encapsulamiento y abriendo la puerta a estrategias de configuración avanzada.

15.8 Riesgos y malas prácticas

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.

15.9 Buenas prácticas de implementación

  • Defina interfaces delgadas y orientadas al dominio para evitar que el Implementor se convierta en un punto de anclaje universal.
  • Documente claramente qué aspectos pueden variar en cada jerarquía para que futuros desarrolladores mantengan la separación.
  • Combine Bridge con inyección de dependencias para seleccionar implementaciones en configuración, lo que habilita escenarios de prueba y despliegues flexibles.
  • Integre mecanismos de monitoreo o logging en la abstracción para observar cambios de implementación sin modificar a los consumidores.

15.10 Relación con otros patrones

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.