Patrón Bridge en Java: desacoplar abstracción de implementación

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 18 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🌉 ¿Qué es el patrón Bridge?

El patrón Bridge (Puente) es uno de los 23 patrones de diseño catalogados por el Gang of Four (GoF) en su obra clásica Design Patterns: Elements of Reusable Object-Oriented Software (1994). Pertenece a la categoría de patrones estructurales y su propósito es desacoplar una abstracción de su implementación, de manera que ambas puedan variar de forma independiente sin afectarse mutuamente.

En términos sencillos, imagina que tienes dos dimensiones que pueden cambiar en tu diseño: qué hace un objeto (la abstracción) y cómo lo hace (la implementación). Sin Bridge, cada combinación de «qué» y «cómo» requeriría una clase nueva. Con Bridge, separas ambas dimensiones en jerarquías independientes conectadas por composición en lugar de herencia.

📘 Definición formal (GoF): «Desacopla una abstracción de su implementación de modo que ambas puedan variar independientemente.» El término bridge (puente) hace referencia al objeto que conecta ambas jerarquías.

Este patrón resulta especialmente valioso cuando un sistema necesita soportar múltiples plataformas, múltiples formatos de salida o múltiples proveedores de un mismo servicio. En Java, el ejemplo más conocido de Bridge es la propia arquitectura JDBC, donde la API estándar (abstracción) se conecta con drivers de distintos fabricantes (implementación) sin que el código de la aplicación cambie.

⚡ El problema que resuelve Bridge

Para entender por qué necesitamos Bridge, consideremos un escenario concreto. Supongamos que estamos desarrollando un sistema de notificaciones para una aplicación empresarial. Tenemos dos dimensiones independientes:

Dimensión 1 — Tipo de notificación: notificación urgente, notificación informativa, notificación de recordatorio.

Dimensión 2 — Canal de envío: email, SMS, notificación push.

❌ Sin Bridge: la explosión combinatoria

Si usamos herencia directa para modelar todas las combinaciones, necesitamos una clase para cada par:

Explosión de clases sin Bridge
// 3 tipos × 3 canales = 9 clases (¡y creciendo!)
NotificacionUrgenteEmail
NotificacionUrgenteSMS
NotificacionUrgentePush
NotificacionInformativaEmail
NotificacionInformativaSMS
NotificacionInformativaPush
NotificacionRecordatorioEmail
NotificacionRecordatorioSMS
NotificacionRecordatorioPush

// Si añadimos un 4º canal (Slack), necesitamos 3 clases más.
// Si añadimos un 4º tipo, necesitamos 4 clases más.
// Complejidad: M × N clases.

Cada vez que añadimos un tipo o un canal, el número de clases crece multiplicativamente. Con 5 tipos y 5 canales tendríamos 25 clases, la mayoría con código duplicado. Esto viola los principios DRY (Don't Repeat Yourself) y OCP (Open/Closed Principle).

✅ Con Bridge: crecimiento lineal

Bridge resuelve el problema separando ambas dimensiones en dos jerarquías independientes. Los tipos de notificación forman una jerarquía (la abstracción) y los canales de envío forman otra (la implementación). Un «puente» —una referencia por composición— las conecta.

Con Bridge: M + N clases
// Abstracción: 3 clases (tipos)
Notificacion (abstracta) → contiene referencia a CanalEnvio
  ├── NotificacionUrgente
  ├── NotificacionInformativa
  └── NotificacionRecordatorio

// Implementación: 3 clases (canales)
CanalEnvio (interfaz)
  ├── CanalEmail
  ├── CanalSMS
  └── CanalPush

// Total: 3 + 3 = 6 clases (en vez de 9)
// Con 5 tipos + 5 canales: 5 + 5 = 10 clases (en vez de 25)
✅ Ventaja clave: La complejidad pasa de M × N a M + N. Cada nueva variante solo requiere añadir una clase a la jerarquía correspondiente, sin tocar la otra dimensión.

📐 Estructura UML del patrón Bridge

El diagrama de clases del patrón Bridge muestra las dos jerarquías conectadas por composición. La clave está en que la abstracción mantiene una referencia a un objeto de la interfaz implementadora, y delega en él las operaciones de bajo nivel:

Diagrama UML — Patrón Bridge (texto)
┌──────────────────────┐         ┌────────────────────────┐
│    «abstract»        │         │    «interface»          │
│    Abstraction       │────────▶│    Implementor          │
├──────────────────────┤  tiene  ├────────────────────────┤
│ - impl: Implementor  │         │ + operationImpl(): void │
├──────────────────────┤         └───────────┬────────────┘
│ + operation(): void  │                     │
└──────────┬───────────┘            ┌────────┴────────┐
           │                        │                 │
  ┌────────┴──────────┐    ┌───────┴──────┐  ┌───────┴──────┐
  │ RefinedAbstraction │    │ ConcreteImplA│  │ ConcreteImplB│
  ├───────────────────┤    ├─────────────┤  ├─────────────┤
  │ + operation()     │    │+operationImpl│  │+operationImpl│
  └───────────────────┘    └──────────────┘  └──────────────┘

Flujo: RefinedAbstraction.operation() → impl.operationImpl()

Observa cómo la Abstraction no hereda de Implementor: la relación es de composición (tiene un), no de herencia (es un). Este es el «puente» que da nombre al patrón y lo que permite la evolución independiente de ambas jerarquías.

🧩 Participantes y responsabilidades

El patrón Bridge define cuatro roles claramente diferenciados. Comprender cada uno es fundamental para aplicar el patrón correctamente:

Participante Rol Responsabilidad
Abstraction Abstracción base Define la interfaz de alto nivel. Mantiene una referencia al Implementor y delega en él las operaciones de bajo nivel.
RefinedAbstraction Abstracción refinada Extiende la Abstraction con variantes de comportamiento de alto nivel. Puede añadir lógica propia antes o después de delegar.
Implementor Interfaz implementadora Define la interfaz para las operaciones de bajo nivel. No tiene que coincidir con la interfaz de Abstraction.
ConcreteImplementor Implementación concreta Proporciona una implementación específica del Implementor. Cada variante de plataforma, formato o proveedor es un ConcreteImplementor.
📘 Nota sobre las interfaces: En Java moderno, el Implementor suele ser una interface (preferido) o una clase abstracta. La Abstraction suele ser una clase abstracta porque necesita mantener estado (la referencia al implementor) y puede contener lógica compartida.

💻 Ejemplo básico: sistema de notificaciones

Vamos a implementar en Java el escenario de notificaciones que analizamos antes. Este ejemplo muestra de forma clara cómo se construye cada participante del patrón Bridge.

▶️ Paso 1: Definir el Implementor (interfaz de canal)

CanalEnvio.java — Implementor
/**
 * Implementor: define las operaciones de bajo nivel
 * para enviar mensajes por un canal específico.
 */
public interface CanalEnvio {

    /**
     * Envía un mensaje al destinatario.
     * @param destinatario dirección, número o token del receptor
     * @param mensaje      contenido del mensaje
     */
    void enviarMensaje(String destinatario, String mensaje);
}

▶️ Paso 2: Implementaciones concretas (ConcreteImplementor)

CanalEmail.java — ConcreteImplementor A
public class CanalEmail implements CanalEnvio {

    @Override
    public void enviarMensaje(String destinatario, String mensaje) {
        System.out.println("[EMAIL] Enviando a " + destinatario);
        System.out.println("  Asunto: Notificación del sistema");
        System.out.println("  Cuerpo: " + mensaje);
        System.out.println("  --- Email enviado correctamente ---");
    }
}
CanalSMS.java — ConcreteImplementor B
public class CanalSMS implements CanalEnvio {

    @Override
    public void enviarMensaje(String destinatario, String mensaje) {
        System.out.println("[SMS] Enviando a " + destinatario);
        // Los SMS tienen límite de 160 caracteres
        String textoSMS = mensaje.length() > 160
            ? mensaje.substring(0, 157) + "..."
            : mensaje;
        System.out.println("  Texto: " + textoSMS);
        System.out.println("  --- SMS enviado correctamente ---");
    }
}
CanalPush.java — ConcreteImplementor C
public class CanalPush implements CanalEnvio {

    @Override
    public void enviarMensaje(String destinatario, String mensaje) {
        System.out.println("[PUSH] Enviando a dispositivo: " + destinatario);
        System.out.println("  Payload: {\"alert\": \"" + mensaje + "\"}");
        System.out.println("  --- Push enviada correctamente ---");
    }
}

▶️ Paso 3: Definir la Abstraction

Notificacion.java — Abstraction
/**
 * Abstraction: define la interfaz de alto nivel para las
 * notificaciones. Mantiene una referencia al canal de envío
 * (Implementor) a través de composición.
 */
public abstract class Notificacion {

    // El "puente" hacia la implementación
    protected final CanalEnvio canal;

    /**
     * Constructor que recibe la implementación por inyección.
     * @param canal el canal de envío a utilizar
     */
    protected Notificacion(CanalEnvio canal) {
        this.canal = canal;
    }

    /**
     * Método de alto nivel: enviar la notificación.
     * Cada tipo de notificación define su propia lógica,
     * pero delega el envío real al canal (Implementor).
     */
    public abstract void enviar(String destinatario, String contenido);
}

▶️ Paso 4: Abstracciones refinadas (RefinedAbstraction)

NotificacionUrgente.java — RefinedAbstraction A
public class NotificacionUrgente extends Notificacion {

    public NotificacionUrgente(CanalEnvio canal) {
        super(canal);
    }

    @Override
    public void enviar(String destinatario, String contenido) {
        String mensajeFormateado = "🚨 [URGENTE] " + contenido.toUpperCase();
        System.out.println("--- Notificación URGENTE ---");
        canal.enviarMensaje(destinatario, mensajeFormateado);
    }
}
NotificacionInformativa.java — RefinedAbstraction B
public class NotificacionInformativa extends Notificacion {

    public NotificacionInformativa(CanalEnvio canal) {
        super(canal);
    }

    @Override
    public void enviar(String destinatario, String contenido) {
        String mensajeFormateado = "ℹ️ [INFO] " + contenido;
        System.out.println("--- Notificación informativa ---");
        canal.enviarMensaje(destinatario, mensajeFormateado);
    }
}

▶️ Paso 5: Programa principal — composición en acción

BridgeDemo.java — Programa principal
public class BridgeDemo {

    public static void main(String[] args) {

        // Crear implementaciones (canales)
        CanalEnvio email = new CanalEmail();
        CanalEnvio sms   = new CanalSMS();
        CanalEnvio push  = new CanalPush();

        // Combinar abstracciones con implementaciones libremente
        Notificacion urgenteEmail = new NotificacionUrgente(email);
        Notificacion urgenteSMS   = new NotificacionUrgente(sms);
        Notificacion infoEmail    = new NotificacionInformativa(email);
        Notificacion infoPush     = new NotificacionInformativa(push);

        // Usar las combinaciones
        urgenteEmail.enviar("ana@empresa.com", "Servidor caído");
        System.out.println();

        urgenteSMS.enviar("+34600123456", "Servidor caído");
        System.out.println();

        infoEmail.enviar("equipo@empresa.com", "Backup completado con éxito");
        System.out.println();

        infoPush.enviar("device-token-abc123", "Nuevo informe disponible");
    }
}

📋 Salida esperada

Salida en consola
--- Notificación URGENTE ---
[EMAIL] Enviando a ana@empresa.com
  Asunto: Notificación del sistema
  Cuerpo: 🚨 [URGENTE] SERVIDOR CAÍDO
  --- Email enviado correctamente ---

--- Notificación URGENTE ---
[SMS] Enviando a +34600123456
  Texto: 🚨 [URGENTE] SERVIDOR CAÍDO
  --- SMS enviado correctamente ---

--- Notificación informativa ---
[EMAIL] Enviando a equipo@empresa.com
  Asunto: Notificación del sistema
  Cuerpo: ℹ️ [INFO] Backup completado con éxito
  --- Email enviado correctamente ---

--- Notificación informativa ---
[PUSH] Enviando a dispositivo: device-token-abc123
  Payload: {"alert": "ℹ️ [INFO] Nuevo informe disponible"}
  --- Push enviada correctamente ---
✅ Observa el poder de Bridge: No existe ninguna clase NotificacionUrgenteEmail ni NotificacionInformativaPush. La combinación se realiza en tiempo de ejecución mediante composición. Si mañana necesitas un canal de Slack, solo creas CanalSlack y funciona con todas las notificaciones existentes sin modificar nada.

🔧 Ejemplo avanzado: sistema de dispositivos y controles remotos

Este segundo ejemplo, más cercano a un proyecto real, modela un sistema donde distintos controles remotos (abstracción) pueden manejar distintos dispositivos electrónicos (implementación). Este es el ejemplo clásico que aparece en la mayoría de libros de patrones de diseño.

▶️ Implementor: interfaz de dispositivo

Dispositivo.java — Implementor
/**
 * Implementor: define las operaciones primitivas
 * que cualquier dispositivo electrónico debe soportar.
 */
public interface Dispositivo {

    boolean estaEncendido();
    void encender();
    void apagar();

    int getVolumen();
    void setVolumen(int porcentaje);

    int getCanal();
    void setCanal(int canal);

    /** Información del dispositivo para diagnóstico */
    String getNombre();
}

▶️ Implementaciones concretas

Television.java — ConcreteImplementor
public class Television implements Dispositivo {

    private boolean encendido = false;
    private int volumen = 30;
    private int canal = 1;

    @Override public boolean estaEncendido() { return encendido; }
    @Override public void encender()  { encendido = true;  }
    @Override public void apagar()    { encendido = false; }

    @Override public int getVolumen() { return volumen; }
    @Override public void setVolumen(int porcentaje) {
        this.volumen = Math.max(0, Math.min(100, porcentaje));
    }

    @Override public int getCanal() { return canal; }
    @Override public void setCanal(int canal) {
        this.canal = Math.max(1, canal);
    }

    @Override public String getNombre() { return "Televisión Samsung 55\""; }
}
Radio.java — ConcreteImplementor
public class Radio implements Dispositivo {

    private boolean encendido = false;
    private int volumen = 20;
    private int emisora = 88; // FM 88.0

    @Override public boolean estaEncendido() { return encendido; }
    @Override public void encender()  { encendido = true;  }
    @Override public void apagar()    { encendido = false; }

    @Override public int getVolumen() { return volumen; }
    @Override public void setVolumen(int porcentaje) {
        this.volumen = Math.max(0, Math.min(100, porcentaje));
    }

    @Override public int getCanal() { return emisora; }
    @Override public void setCanal(int canal) {
        this.emisora = Math.max(88, Math.min(108, canal));
    }

    @Override public String getNombre() { return "Radio FM Digital"; }
}

▶️ Abstracción: control remoto

ControlRemoto.java — Abstraction
/**
 * Abstraction: control remoto básico.
 * Mantiene referencia al dispositivo (Implementor)
 * y define operaciones de alto nivel.
 */
public class ControlRemoto {

    protected final Dispositivo dispositivo;

    public ControlRemoto(Dispositivo dispositivo) {
        this.dispositivo = dispositivo;
    }

    public void toggleEncendido() {
        if (dispositivo.estaEncendido()) {
            dispositivo.apagar();
            System.out.println(dispositivo.getNombre() + " → APAGADO");
        } else {
            dispositivo.encender();
            System.out.println(dispositivo.getNombre() + " → ENCENDIDO");
        }
    }

    public void subirVolumen() {
        dispositivo.setVolumen(dispositivo.getVolumen() + 10);
        System.out.println("Volumen: " + dispositivo.getVolumen());
    }

    public void bajarVolumen() {
        dispositivo.setVolumen(dispositivo.getVolumen() - 10);
        System.out.println("Volumen: " + dispositivo.getVolumen());
    }

    public void canalArriba() {
        dispositivo.setCanal(dispositivo.getCanal() + 1);
        System.out.println("Canal: " + dispositivo.getCanal());
    }

    public void canalAbajo() {
        dispositivo.setCanal(dispositivo.getCanal() - 1);
        System.out.println("Canal: " + dispositivo.getCanal());
    }
}

▶️ Abstracción refinada: control avanzado

ControlRemotoAvanzado.java — RefinedAbstraction
/**
 * RefinedAbstraction: añade funciones extra como
 * silenciar y mostrar estado detallado del dispositivo.
 */
public class ControlRemotoAvanzado extends ControlRemoto {

    private int volumenAnterior = -1;

    public ControlRemotoAvanzado(Dispositivo dispositivo) {
        super(dispositivo);
    }

    /** Silencia el dispositivo o restaura el volumen anterior */
    public void silenciar() {
        if (volumenAnterior < 0) {
            volumenAnterior = dispositivo.getVolumen();
            dispositivo.setVolumen(0);
            System.out.println("🔇 Silenciado (volumen anterior: " + volumenAnterior + ")");
        } else {
            dispositivo.setVolumen(volumenAnterior);
            System.out.println("🔊 Volumen restaurado a " + volumenAnterior);
            volumenAnterior = -1;
        }
    }

    /** Muestra el estado completo del dispositivo */
    public void mostrarEstado() {
        System.out.println("┌──────────────────────────────┐");
        System.out.println("│ Dispositivo: " + dispositivo.getNombre());
        System.out.println("│ Estado:      " + (dispositivo.estaEncendido() ? "Encendido" : "Apagado"));
        System.out.println("│ Volumen:     " + dispositivo.getVolumen() + "%");
        System.out.println("│ Canal:       " + dispositivo.getCanal());
        System.out.println("└──────────────────────────────┘");
    }
}

▶️ Programa principal

DispositivosDemo.java
public class DispositivosDemo {

    public static void main(String[] args) {

        // Control básico → Televisión
        System.out.println("=== Control básico + TV ===");
        ControlRemoto controlTV = new ControlRemoto(new Television());
        controlTV.toggleEncendido();
        controlTV.subirVolumen();
        controlTV.canalArriba();

        System.out.println();

        // Control avanzado → Radio
        System.out.println("=== Control avanzado + Radio ===");
        ControlRemotoAvanzado controlRadio =
            new ControlRemotoAvanzado(new Radio());
        controlRadio.toggleEncendido();
        controlRadio.subirVolumen();
        controlRadio.silenciar();
        controlRadio.silenciar();  // Restaurar
        controlRadio.mostrarEstado();
    }
}

🌐 Bridge en el mundo real: JDBC y la API de Java

El ejemplo más relevante del patrón Bridge en el ecosistema Java es la arquitectura JDBC (Java Database Connectivity). JDBC es un Bridge canónico donde la API estándar de Java actúa como abstracción y los drivers de cada fabricante actúan como implementación:

Rol en Bridge En JDBC Descripción
Abstraction java.sql.DriverManager Punto de entrada de alto nivel para obtener conexiones.
RefinedAbstraction java.sql.Connection, Statement, ResultSet Interfaces refinadas que el programador usa para interactuar con la BD.
Implementor java.sql.Driver Interfaz que cada driver de BD debe implementar.
ConcreteImplementor com.mysql.cj.jdbc.Driver, org.postgresql.Driver Drivers específicos de cada fabricante de base de datos.
JDBC como Bridge — Código de ejemplo
// Tu código (usa la abstracción) — NO cambia al cambiar de BD
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/tienda", "usuario", "clave"
);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM productos");

// Para cambiar de MySQL a PostgreSQL, solo cambias la URL:
// "jdbc:postgresql://localhost:5432/tienda"
// El resto del código permanece EXACTAMENTE igual.
// Eso es Bridge en acción.

Otros ejemplos reales de Bridge en Java incluyen la API de SLF4J para logging (abstracción) con backends como Logback o Log4j (implementación), los drivers de AWT en java.awt.Toolkit, y la arquitectura de Java Cryptography Architecture (JCA) donde los proveedores de seguridad (Bouncy Castle, SunJCE) implementan algoritmos intercambiables.

⚖️ Bridge vs. otros patrones estructurales

Bridge puede confundirse con otros patrones que también usan composición y delegación. Esta tabla aclara las diferencias fundamentales:

Patrón Propósito Diferencia con Bridge
Adapter Hacer compatible una interfaz existente con otra que el cliente espera. Adapter trabaja con clases ya existentes que no pueden modificarse. Bridge se diseña desde el inicio para permitir la evolución independiente.
Strategy Intercambiar algoritmos dentro de un mismo contexto. Strategy opera en una sola dimensión (algoritmos). Bridge opera en dos dimensiones ortogonales (abstracción + implementación). Bridge puede contener Strategies dentro.
Decorator Añadir responsabilidades dinámicamente a un objeto. Decorator envuelve el mismo tipo de interfaz en capas. Bridge conecta dos interfaces distintas.
Abstract Factory Crear familias de objetos relacionados. Abstract Factory puede usarse para crear los ConcreteImplementors de un Bridge. Son complementarios, no competidores.
⚠️ Error frecuente: No confundas Bridge con Adapter. La diferencia clave es la intención temporal. Bridge se diseña al principio de un proyecto para permitir flexibilidad futura. Adapter se aplica después para integrar componentes incompatibles que ya existen.

🎯 Cuándo usar y cuándo evitar Bridge

✅ Usa Bridge cuando:

Tienes dos dimensiones que varían independientemente. Si identificas que tu sistema tiene al menos dos ejes de variación (por ejemplo, plataforma × funcionalidad, formato × contenido, proveedor × servicio), Bridge es probablemente la solución correcta.

Quieres evitar la explosión combinatoria de subclases. Si al dibujar tu diagrama de herencia ves que el número de clases crece multiplicativamente, es una señal clara de que necesitas Bridge.

Necesitas poder cambiar implementaciones en tiempo de ejecución. Gracias a la composición, puedes asignar un nuevo implementor al mismo objeto abstracción sin recrearlo.

Desarrollas software multiplataforma o multi-proveedor. Renderizadores gráficos (OpenGL/Vulkan/DirectX), drivers de base de datos (JDBC), o APIs de mensajería (email/SMS/push) son candidatos naturales.

❌ Evita Bridge cuando:

Solo tienes una dimensión de variación. Si el «cómo» no varía, la herencia simple o el patrón Strategy son más sencillos y apropiados.

La implementación nunca cambiará. Si estás seguro de que solo existirá un proveedor, formato o plataforma, la capa de abstracción adicional de Bridge añade complejidad innecesaria.

El sistema es pequeño y con pocas combinaciones. Si solo tienes 2×2 = 4 combinaciones y no prevés crecimiento, Bridge puede ser sobreingeniería. Aplica el principio YAGNI (You Aren't Gonna Need It).

🐛 Errores comunes al implementar Bridge

Incluso desarrolladores experimentados cometen estos errores al aplicar Bridge. Conocerlos te ayudará a evitarlos:

Error Problema Solución
Confundir abstracción con implementación Poner lógica de bajo nivel en la abstracción o lógica de alto nivel en el implementor. La abstracción define qué hacer; el implementor define cómo. Si la Abstraction contiene detalles de plataforma, estás mezclando niveles.
Interfaces de implementor demasiado amplias El implementor acaba replicando la API de la abstracción. El implementor solo debe exponer operaciones primitivas de bajo nivel. La abstracción las orquesta en operaciones de alto nivel.
No inyectar la implementación Crear el ConcreteImplementor dentro de la Abstraction con new. Siempre inyectar el implementor por constructor o método setter. Opcionalmente, usar un Factory para la creación.
Acoplar las jerarquías Una RefinedAbstraction hace instanceof sobre el ConcreteImplementor. Las dos jerarquías deben ser completamente opacas entre sí. Si necesitas saber el tipo concreto, el diseño del puente está roto.

📝 Ejercicios resueltos

🏋️ Ejercicio 1: Sistema de formas y renderizadores

Enunciado: Diseña un sistema con el patrón Bridge donde distintas formas geométricas (círculo, rectángulo) puedan renderizarse con distintos motores de renderizado (renderizado vectorial SVG, renderizado raster PNG). Implementa las interfaces, clases concretas y un programa principal que demuestre las combinaciones.

Solución: Formas y Renderizadores
// Implementor
public interface Renderizador {
    void renderizarCirculo(double radio);
    void renderizarRectangulo(double ancho, double alto);
}

// ConcreteImplementor A
public class RenderizadorSVG implements Renderizador {
    @Override
    public void renderizarCirculo(double radio) {
        System.out.println("SVG: <circle r=\"" + radio + "\" />");
    }
    @Override
    public void renderizarRectangulo(double ancho, double alto) {
        System.out.println("SVG: <rect width=\"" + ancho + "\" height=\"" + alto + "\" />");
    }
}

// ConcreteImplementor B
public class RenderizadorRaster implements Renderizador {
    @Override
    public void renderizarCirculo(double radio) {
        System.out.println("Raster: dibujando " + (int)(2*radio) + "x" + (int)(2*radio) + " px");
    }
    @Override
    public void renderizarRectangulo(double ancho, double alto) {
        System.out.println("Raster: dibujando " + (int)ancho + "x" + (int)alto + " px");
    }
}

// Abstraction
public abstract class Forma {
    protected final Renderizador renderizador;
    public Forma(Renderizador renderizador) {
        this.renderizador = renderizador;
    }
    public abstract void dibujar();
    public abstract void redimensionar(double factor);
}

// RefinedAbstraction A
public class Circulo extends Forma {
    private double radio;
    public Circulo(Renderizador renderizador, double radio) {
        super(renderizador);
        this.radio = radio;
    }
    @Override
    public void dibujar() {
        renderizador.renderizarCirculo(radio);
    }
    @Override
    public void redimensionar(double factor) {
        radio *= factor;
    }
}

// RefinedAbstraction B
public class Rectangulo extends Forma {
    private double ancho, alto;
    public Rectangulo(Renderizador renderizador, double ancho, double alto) {
        super(renderizador);
        this.ancho = ancho;
        this.alto = alto;
    }
    @Override
    public void dibujar() {
        renderizador.renderizarRectangulo(ancho, alto);
    }
    @Override
    public void redimensionar(double factor) {
        ancho *= factor;
        alto *= factor;
    }
}

// Main
public class FormasDemo {
    public static void main(String[] args) {
        Forma circuloSVG = new Circulo(new RenderizadorSVG(), 50);
        Forma circuloPNG = new Circulo(new RenderizadorRaster(), 50);
        Forma rectSVG    = new Rectangulo(new RenderizadorSVG(), 100, 60);

        circuloSVG.dibujar();     // SVG: <circle r="50.0" />
        circuloPNG.dibujar();     // Raster: dibujando 100x100 px
        rectSVG.dibujar();        // SVG: <rect width="100.0" height="60.0" />

        circuloSVG.redimensionar(2.0);
        circuloSVG.dibujar();     // SVG: <circle r="100.0" />
    }
}

🏋️ Ejercicio 2: Sistema de reportes multi-formato

Enunciado: Una empresa necesita generar reportes de distintos tipos (reporte financiero, reporte de inventario) en distintos formatos de salida (HTML, PDF texto plano, CSV). Aplica Bridge para diseñar la solución. Implementa al menos dos tipos de reporte y dos formatos.

Solución: Reportes multi-formato
// Implementor: formato de salida
public interface FormatoSalida {
    void abrirDocumento(String titulo);
    void escribirSeccion(String encabezado, String contenido);
    void escribirTabla(String[] cabeceras, String[][] datos);
    void cerrarDocumento();
}

// ConcreteImplementor A: HTML
public class FormatoHTML implements FormatoSalida {
    @Override
    public void abrirDocumento(String titulo) {
        System.out.println("<html><head><title>" + titulo + "</title></head><body>");
        System.out.println("<h1>" + titulo + "</h1>");
    }
    @Override
    public void escribirSeccion(String encabezado, String contenido) {
        System.out.println("<h2>" + encabezado + "</h2>");
        System.out.println("<p>" + contenido + "</p>");
    }
    @Override
    public void escribirTabla(String[] cabeceras, String[][] datos) {
        System.out.println("<table border='1'><tr>");
        for (String h : cabeceras) System.out.print("<th>" + h + "</th>");
        System.out.println("</tr>");
        for (String[] fila : datos) {
            System.out.print("<tr>");
            for (String celda : fila) System.out.print("<td>" + celda + "</td>");
            System.out.println("</tr>");
        }
        System.out.println("</table>");
    }
    @Override
    public void cerrarDocumento() {
        System.out.println("</body></html>");
    }
}

// ConcreteImplementor B: CSV
public class FormatoCSV implements FormatoSalida {
    @Override
    public void abrirDocumento(String titulo) {
        System.out.println("# " + titulo);
    }
    @Override
    public void escribirSeccion(String encabezado, String contenido) {
        System.out.println("\n## " + encabezado);
        System.out.println(contenido);
    }
    @Override
    public void escribirTabla(String[] cabeceras, String[][] datos) {
        System.out.println(String.join(",", cabeceras));
        for (String[] fila : datos) {
            System.out.println(String.join(",", fila));
        }
    }
    @Override
    public void cerrarDocumento() {
        System.out.println("\n# Fin del reporte");
    }
}

// Abstraction
public abstract class Reporte {
    protected final FormatoSalida formato;
    protected Reporte(FormatoSalida formato) {
        this.formato = formato;
    }
    public abstract void generar();
}

// RefinedAbstraction A
public class ReporteFinanciero extends Reporte {
    public ReporteFinanciero(FormatoSalida formato) {
        super(formato);
    }
    @Override
    public void generar() {
        formato.abrirDocumento("Reporte Financiero Q1 2026");
        formato.escribirSeccion("Resumen ejecutivo",
            "Ingresos totales: 1.200.000€. Beneficio neto: 340.000€.");
        formato.escribirTabla(
            new String[]{"Concepto", "Importe"},
            new String[][]{
                {"Ventas", "1.200.000€"},
                {"Costes", "860.000€"},
                {"Beneficio", "340.000€"}
            }
        );
        formato.cerrarDocumento();
    }
}

// RefinedAbstraction B
public class ReporteInventario extends Reporte {
    public ReporteInventario(FormatoSalida formato) {
        super(formato);
    }
    @Override
    public void generar() {
        formato.abrirDocumento("Reporte de Inventario");
        formato.escribirSeccion("Estado del almacén",
            "Total de productos: 3.450 unidades en 12 categorías.");
        formato.escribirTabla(
            new String[]{"Producto", "Stock", "Mínimo"},
            new String[][]{
                {"Portátil HP", "120", "50"},
                {"Monitor Dell", "85", "30"},
                {"Teclado Logitech", "340", "100"}
            }
        );
        formato.cerrarDocumento();
    }
}

// Main
public class ReportesDemo {
    public static void main(String[] args) {
        System.out.println("=== Financiero en HTML ===");
        new ReporteFinanciero(new FormatoHTML()).generar();

        System.out.println("\n=== Inventario en CSV ===");
        new ReporteInventario(new FormatoCSV()).generar();
    }
}

🏋️ Ejercicio 3: Identificar Bridge en código existente

Enunciado: Analiza el siguiente fragmento y determina si aplica correctamente el patrón Bridge. Si contiene errores, identifícalos y propón la corrección.

Código a analizar
public abstract class Ventana {
    public void dibujar() {
        VentanaImpl impl = new VentanaWindows(); // ← Atención aquí
        impl.dibujarVentana();
    }
}
public interface VentanaImpl {
    void dibujarVentana();
}
public class VentanaWindows implements VentanaImpl { ... }
public class VentanaLinux implements VentanaImpl { ... }

Error principal: La Abstraction crea directamente la implementación concreta (new VentanaWindows()) dentro de su propio método. Esto acopla fuertemente la abstracción a una implementación específica, destruyendo la esencia del patrón Bridge.

Corrección:

Versión corregida
public abstract class Ventana {
    protected final VentanaImpl impl; // Inyección por constructor

    protected Ventana(VentanaImpl impl) {
        this.impl = impl;
    }

    public void dibujar() {
        impl.dibujarVentana(); // Delega sin saber el tipo concreto
    }
}

// Uso: new VentanaDialogo(new VentanaLinux());

La implementación se inyecta desde fuera, lo que permite cambiar de VentanaWindows a VentanaLinux sin modificar ninguna clase de la jerarquía de abstracción.

❓ Preguntas frecuentes sobre Patrón Bridge en Java: desacoplar abstracción de implementación

Las dudas más comunes respondidas de forma clara y directa.

Ambos usan composición para delegar trabajo, pero Bridge separa una abstracción de su implementación para que evolucionen independientemente, mientras que Strategy permite intercambiar algoritmos dentro de un mismo contexto. Bridge opera en dos jerarquías paralelas; Strategy trabaja con una sola familia de algoritmos.
No exactamente. Una interfaz Java es un mecanismo del lenguaje, mientras que Bridge es un patrón de diseño que usa interfaces (o clases abstractas) como herramienta. Bridge define una relación específica entre una abstracción y su implementación, creando dos jerarquías independientes conectadas por composición.
Sí, Bridge se combina frecuentemente con otros patrones. Se puede usar Abstract Factory para crear las implementaciones concretas, Adapter para hacer compatibles implementaciones existentes, o Strategy dentro de la implementación. En sistemas grandes, es habitual ver Bridge trabajando junto con Builder o Singleton.
Sí, JDBC es el ejemplo más conocido de Bridge en Java. La API JDBC (DriverManager, Connection, Statement) actúa como la abstracción, y los drivers de cada fabricante (MySQL Connector, PostgreSQL JDBC, Oracle JDBC) son las implementaciones concretas. Puedes cambiar de base de datos sin modificar tu código de aplicación.
No uses Bridge si solo tienes una implementación posible y no prevés cambios, si la complejidad adicional de dos jerarquías no se justifica, o si el problema se resuelve mejor con herencia simple. Bridge añade capas de abstracción, así que solo merece la pena cuando necesitas que ambas dimensiones (abstracción e implementación) varíen de forma independiente.
Puede parecerlo en ejemplos sencillos, porque añade interfaces y clases extra. Sin embargo, en sistemas reales con múltiples variantes de abstracción e implementación, Bridge reduce drásticamente la explosión combinatoria de clases. Un sistema sin Bridge podría necesitar M×N clases, mientras que con Bridge solo necesita M+N. La complejidad se paga al inicio pero se ahorra con creces a medida que el sistema crece.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Patrón Bridge en Java: desacoplar abstracción de implementación? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!