Abby's Digital Cafe

Patrón Estructural - Bridge

por Abigail Palmero (Abbytec)

¿Qué es el patrón Bridge?

Bridge (Puente) es un patrón de diseño estructural que te ayuda a separar dos cosas que suelen crecer juntas: lo que tu objeto “hace” (la abstracción) y cómo lo hace (la implementación). En vez de mezclar todo en una sola jerarquía de clases, Bridge crea dos “familias” de clases que se conectan por composición (un objeto contiene a otro). Así, puedes cambiar la implementación sin tocar la abstracción, y viceversa.

Una forma simple de entenderlo: imagina un control remoto (abstracción) que puede manejar distintas marcas de TV (implementaciones). No quieres reescribir el control remoto cada vez que cambias de TV; quieres “enchufar” una implementación diferente.

El problema típico: la “explosión de clases”

Cuando tienes dos dimensiones que varían (por ejemplo: tipo de notificación y canal de envío), la solución ingenua es crear una clase por cada combinación. Eso se vuelve inmanejable rápido: EmailUrgente, SmsUrgente, PushUrgente, EmailNormal, SmsNormal, PushNormal… y así sucesivamente. Bridge evita esto separando “tipo de mensaje” de “canal de envío”.

Cómo identificar Bridge en tu código (pasos claros)

  1. Detecta dos cosas que cambian de forma independiente (por ejemplo: “qué” haces y “con qué” lo haces).
  2. Define una interfaz o clase base para la implementación (por ejemplo, "Sender" o "Renderer").
  3. Crea implementaciones concretas (por ejemplo: EmailSender, SmsSender).
  4. Define la abstracción (por ejemplo: Notification) que usa una implementación (tiene una propiedad sender/renderer).
  5. Extiende la abstracción si necesitas variantes del “qué” (por ejemplo: Alerta, Recordatorio) sin duplicar implementaciones.
  6. Prueba que puedes cambiar la implementación sin modificar la abstracción (inyectando otra implementación).

Preguntas frecuentes (con respuestas)

¿Bridge es lo mismo que Strategy?

Se parecen porque ambos usan composición (un objeto delega trabajo a otro). La diferencia práctica es la intención: Strategy suele enfocarse en intercambiar un algoritmo (cómo se hace una parte específica). Bridge se enfoca en separar dos jerarquías completas que pueden evolucionar por separado (abstracción e implementación). Si tu problema es “tengo dos ejes que combinados crean demasiadas clases”, Bridge suele encajar muy bien.

¿Cuándo NO usar Bridge?

Si solo tienes una implementación y sabes que no cambiará, Bridge puede ser sobreingeniería: más archivos y más capas sin beneficio real. También evita usarlo si el equipo aún no está cómodo con interfaces/clases separadas; en ese caso conviene mantenerlo simple hasta que aparezca la necesidad.

¿Qué gano al separar abstracción e implementación?

Ganas flexibilidad y menos duplicación. Puedes agregar un nuevo “tipo” en la abstracción sin tocar los canales/implementaciones, o agregar una nueva implementación sin tocar los tipos. Esto reduce el riesgo de romper cosas y hace tu código más fácil de ampliar.


Caso de uso 1 (TypeScript): notificaciones con distintos canales

Escenario realista: estás construyendo una app y necesitas enviar notificaciones. Hay distintos tipos de notificación (por ejemplo, “Alerta” y “Recordatorio”) y distintos canales (Email y SMS). Bridge te permite mezclar ambos sin crear una clase por combinación.

[typescript]
interface NotifierChannel { send(to: string, message: string): void; } class EmailChannel implements NotifierChannel { send(to: string, message: string): void { console.log(`[EMAIL] To: ${to} -> ${message}`); } } class SmsChannel implements NotifierChannel { send(to: string, message: string): void { console.log(`[SMS] To: ${to} -> ${message}`); } } // Abstracción abstract class Notification { constructor(protected channel: NotifierChannel) {} abstract notify(to: string, text: string): void; } // Variantes de la abstracción class AlertNotification extends Notification { notify(to: string, text: string): void { this.channel.send(to, `ALERTA: ${text}`); } } class ReminderNotification extends Notification { notify(to: string, text: string): void { this.channel.send(to, `Recordatorio: ${text}`); } } // Uso const email = new EmailChannel(); const sms = new SmsChannel(); const alertByEmail = new AlertNotification(email); alertByEmail.notify("[email protected]", "Tu pago fue rechazado."); const reminderBySms = new ReminderNotification(sms); reminderBySms.notify("+5491112345678", "Tu turno es mañana a las 10:00."); // Cambiar el canal sin cambiar la abstracción const alertBySms = new AlertNotification(sms); alertBySms.notify("+5491112345678", "Se detectó un inicio de sesión nuevo.");

Observa lo importante: AlertNotification y ReminderNotification no saben si el envío es por email o SMS. Solo piden “canal, envía esto”. Eso permite agregar un PushChannel mañana sin tocar tus clases de notificación.

Caso de uso 2 (Java): reportes con distintos formatos y destinos

Escenario realista: en una empresa, necesitas generar reportes (por ejemplo, “Reporte de Ventas” y “Reporte de Inventario”) y exportarlos en distintos formatos (PDF o CSV). Además, quieres que los reportes puedan crecer como “tipos” sin duplicar el código de exportación.

[java]
import java.util.List; interface Exporter { String export(String title, List<String> rows); } class PdfExporter implements Exporter { @Override public String export(String title, List<String> rows) { // Simplificado: en un caso real aquí usarías una librería de PDF. StringBuilder sb = new StringBuilder(); sb.append("[PDF]\n"); sb.append("# ").append(title).append("\n"); for (String row : rows) sb.append("- ").append(row).append("\n"); return sb.toString(); } } class CsvExporter implements Exporter { @Override public String export(String title, List<String> rows) { // Simplificado: CSV básico StringBuilder sb = new StringBuilder(); sb.append("title,").append(title).append("\n"); for (String row : rows) sb.append("row,").append(row).append("\n"); return sb.toString(); } } // Abstracción abstract class Report { protected final Exporter exporter; protected Report(Exporter exporter) { this.exporter = exporter; } public abstract String build(); } // Variantes de la abstracción class SalesReport extends Report { public SalesReport(Exporter exporter) { super(exporter); } @Override public String build() { List<String> rows = List.of( "Ventas total: 120", "Ticket promedio: 15", "Sucursal destacada: Centro" ); return exporter.export("Reporte de Ventas", rows); } } class InventoryReport extends Report { public InventoryReport(Exporter exporter) { super(exporter); } @Override public String build() { List<String> rows = List.of( "SKU-123: 40 unidades", "SKU-456: 0 unidades (reponer)", "SKU-789: 12 unidades" ); return exporter.export("Reporte de Inventario", rows); } } public class Main { public static void main(String[] args) { Exporter pdf = new PdfExporter(); Exporter csv = new CsvExporter(); Report salesAsPdf = new SalesReport(pdf); System.out.println(salesAsPdf.build()); Report inventoryAsCsv = new InventoryReport(csv); System.out.println(inventoryAsCsv.build()); // Cambiar exportador sin tocar el tipo de reporte Report salesAsCsv = new SalesReport(csv); System.out.println(salesAsCsv.build()); } }

Aquí el “tipo de reporte” (SalesReport, InventoryReport) está separado del “cómo lo exporto” (PdfExporter, CsvExporter). Si mañana agregas ExcelExporter, no necesitas reescribir los reportes. Y si mañana agregas CustomerReport, puedes exportarlo en PDF o CSV sin duplicar lógica.

Checklist rápido para usar Bridge en tu proyecto

  • Tengo dos ejes que cambian por separado (por ejemplo, tipo y canal).
  • Estoy viendo nombres de clases por combinación (señal de explosión).
  • Quiero poder agregar una implementación nueva sin tocar el resto.
  • Puedo definir una interfaz clara para la implementación.
  • Me conviene probar cambiando implementaciones en runtime (inyectando dependencias).

Cierre: la idea clave para recordarlo

Quédate con esta frase: Bridge separa “qué hago” de “cómo lo hago”. Si tu sistema está creciendo y sientes que cada nueva variante te obliga a duplicar clases, Bridge puede ayudarte a mantener el código ordenado, flexible y más fácil de extender sin reescribirlo todo.

Comentarios