🌉 ¿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.
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:
// 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.
// 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)
📐 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:
┌──────────────────────┐ ┌────────────────────────┐
│ «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. |
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)
/**
* 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)
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 ---");
}
}
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 ---");
}
}
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
/**
* 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)
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);
}
}
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
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
--- 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 ---
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
/**
* 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
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\""; }
}
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
/**
* 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
/**
* 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
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. |
// 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. |
🎯 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.
// 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.
// 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.
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:
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.
💬 Foro de discusión
¿Tienes dudas sobre Patrón Bridge en Java: desacoplar abstracción de implementación? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!