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 Java

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í.

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:

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.

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.