Adapter cerró la lección anterior con una promesa: dejar de arreglar desencajes a posteriori y aprender a diseñar a priori para que dos dimensiones que van a variar no se suelden nunca. Ese es exactamente el patrón Bridge, y PideYa tiene el caso de libro esperándolo: las notificaciones. Hay tipos de notificación (confirmación de pedido, aviso de retraso, promoción) y hay canales de envío (push, SMS, email). Dos ejes independientes que, modelados con herencia ingenua, se multiplican entre sí. Bridge los separa en dos jerarquías conectadas por un puente, para que cada eje crezca sin enterarse del otro.
Contenido
- El problema en PideYa: la explosión tipo × canal
- Intención y estructura del patrón
- Implementación Java completa
- Crecer por los dos ejes
- Bridge frente a Adapter (y una mención a Strategy)
- Cuándo usarlo y cuándo no
- Errores comunes, ejercicios y conclusión
El problema en PideYa: la explosión tipo × canal
En el módulo 2 resolvimos quién crea los notificadores de cada canal (NotificadorPush, NotificadorSms, NotificadorEmail, elegidos vía RegistroNotificadores). Pero el producto creció: ya no basta "enviar un texto por un canal". Cada tipo de notificación tiene su propia lógica de negocio:
- Confirmación de pedido: incluye número de pedido, resumen de líneas y tiempo estimado; se envía siempre.
- Aviso de retraso: calcula la nueva hora estimada, adjunta una disculpa y, si el retraso supera 20 minutos, un cupón compensatorio.
- Promoción: texto de marketing con condiciones legales; solo se envía a clientes que han dado consentimiento.
El primer intento del equipo fue extender la jerarquía existente por herencia, una subclase por combinación:
// Primer intento: una subclase por CADA combinación. NO imitar.
public class ConfirmacionPush extends NotificadorPush { /* arma el texto de confirmación y lo manda por push */ }
public class ConfirmacionSms extends NotificadorSms { /* el MISMO texto, por SMS */ }
public class ConfirmacionEmail extends NotificadorEmail { /* el MISMO texto, por email */ }
public class RetrasoPush extends NotificadorPush { /* ... */ }
public class RetrasoSms extends NotificadorSms { /* ... */ }
public class RetrasoEmail extends NotificadorEmail { /* ... */ }
public class PromocionPush extends NotificadorPush { /* ... */ }
// ... y siguenCon 3 tipos y 3 canales, 9 clases. Marketing pide notificaciones de "valora tu pedido" (tipo nuevo): +3 clases. Operaciones contrata WhatsApp (canal nuevo): +4 clases (una por cada tipo existente). La jerarquía crece como el producto de los dos ejes: tipos × canales. Y hay un daño peor que la cantidad: la duplicación. La lógica de "cuándo toca cupón compensatorio" está copiada en RetrasoPush, RetrasoSms y RetrasoEmail; el día que cambie el umbral de 20 minutos habrá que tocar tres clases y alguien olvidará una. DRY, dinamitado.
El diagnóstico preciso: la jerarquía única está intentando capturar dos dimensiones de variación independientes —qué se notifica y por dónde se envía— con un solo mecanismo, la herencia, que solo sabe crecer en un eje. La solución no es más herencia: es partir la jerarquía en dos y conectarlas con composición.
Intención y estructura del patrón
Intención (GoF): desacoplar una abstracción de su implementación, de modo que ambas puedan variar independientemente.
Los nombres del GoF despistan, así que traduzcámoslos antes del diagrama:
- Abstracción (Abstraction): la jerarquía orientada al negocio/cliente; aquí, los tipos de notificación (qué se dice, cuándo, con qué reglas).
- Implementación (Implementor): la jerarquía orientada a la plataforma/técnica; aquí, los canales de envío (cómo llega físicamente el mensaje).
- El puente: la abstracción contiene una referencia al implementor y delega en él la parte técnica. Esa flecha de composición entre las dos jerarquías es el patrón entero.
classDiagram
class Notificacion {
<<abstract>>
#canal: CanalEnvio
+Notificacion(canal: CanalEnvio)
+notificar(pedido: Pedido)*
}
class NotificacionConfirmacion {
+notificar(pedido: Pedido)
}
class NotificacionRetraso {
-minutosRetraso: int
+notificar(pedido: Pedido)
}
class NotificacionPromocion {
-campania: Campania
+notificar(pedido: Pedido)
}
class CanalEnvio {
<<interface>>
+enviar(destinatario: Cliente, titulo: String, cuerpo: String)
}
class CanalPush {
+enviar(destinatario, titulo, cuerpo)
}
class CanalSms {
+enviar(destinatario, titulo, cuerpo)
}
class CanalEmail {
+enviar(destinatario, titulo, cuerpo)
}
Notificacion <|-- NotificacionConfirmacion
Notificacion <|-- NotificacionRetraso
Notificacion <|-- NotificacionPromocion
CanalEnvio <|.. CanalPush
CanalEnvio <|.. CanalSms
CanalEnvio <|.. CanalEmail
Notificacion o-- CanalEnvio : puente
| Rol GoF | En PideYa |
|---|---|
| Abstraction | Notificacion (clase base con la referencia al canal) |
| RefinedAbstraction | NotificacionConfirmacion, NotificacionRetraso, NotificacionPromocion |
| Implementor | CanalEnvio |
| ConcreteImplementor | CanalPush, CanalSms, CanalEmail |
Cuenta de la vieja: con Bridge, 3 tipos + 3 canales = 3 + 3 = 6 clases (más una base y una interfaz) en lugar de 3 × 3 = 9. Con 5 tipos y 4 canales: 9 frente a 20. La herencia multiplicaba; la composición suma. Y cada regla de negocio vive en una sola clase de la jerarquía de tipos, escrita una vez.
Implementación Java completa
El implementor y sus canales concretos — deliberadamente tontos: solo saben hacer llegar un título y un cuerpo a un cliente por su medio:
public interface CanalEnvio {
void enviar(Cliente destinatario, String titulo, String cuerpo);
}
public class CanalPush implements CanalEnvio {
@Override
public void enviar(Cliente destinatario, String titulo, String cuerpo) {
// Busca los tokens de dispositivo del cliente y envía vía FCM/APNs
RegistroEventos.INSTANCE.info("Push a " + destinatario.getId() + ": " + titulo);
}
}
public class CanalSms implements CanalEnvio {
@Override
public void enviar(Cliente destinatario, String titulo, String cuerpo) {
// SMS sin título: se concatena, y se recorta a 160 caracteres
String texto = (titulo + ". " + cuerpo);
smsGateway.mandar(destinatario.getTelefono(),
texto.length() <= 160 ? texto : texto.substring(0, 157) + "...");
}
}
public class CanalEmail implements CanalEnvio {
@Override
public void enviar(Cliente destinatario, String titulo, String cuerpo) {
servidorSmtp.mandar(destinatario.getEmail(), titulo, plantillaHtml(cuerpo));
}
}Observa que cada canal resuelve sus particularidades técnicas (el SMS no tiene título y limita caracteres; el email maqueta HTML) sin saber jamás qué está enviando: ¿una confirmación?, ¿una promoción? No es asunto suyo.
La abstracción y sus refinamientos — donde vive el negocio:
public abstract class Notificacion {
protected final CanalEnvio canal; // <- el puente
protected Notificacion(CanalEnvio canal) {
this.canal = canal;
}
/** Cada tipo decide qué comunicar y cuándo; el CÓMO se delega en el canal. */
public abstract void notificar(Pedido pedido);
}
public class NotificacionConfirmacion extends Notificacion {
public NotificacionConfirmacion(CanalEnvio canal) { super(canal); }
@Override
public void notificar(Pedido pedido) {
String titulo = "Pedido " + pedido.getNumero() + " confirmado";
String cuerpo = "Tu pedido de " + pedido.getRestaurante().getNombre()
+ " llegará hacia las " + pedido.getHoraEstimada() + ".";
canal.enviar(pedido.getCliente(), titulo, cuerpo);
}
}
public class NotificacionRetraso extends Notificacion {
private static final int MINUTOS_PARA_CUPON = 20; // la regla, UNA sola vez
private final int minutosRetraso;
public NotificacionRetraso(CanalEnvio canal, int minutosRetraso) {
super(canal);
this.minutosRetraso = minutosRetraso;
}
@Override
public void notificar(Pedido pedido) {
String titulo = "Tu pedido se retrasa " + minutosRetraso + " minutos";
String cuerpo = "Lo sentimos. Nueva hora estimada: "
+ pedido.getHoraEstimada().plusMinutes(minutosRetraso) + ".";
if (minutosRetraso > MINUTOS_PARA_CUPON) {
cuerpo += " Te regalamos un cupón del 10% para tu próximo pedido.";
servicioCupones.emitirCompensacion(pedido.getCliente());
}
canal.enviar(pedido.getCliente(), titulo, cuerpo);
}
}
public class NotificacionPromocion extends Notificacion {
private final Campania campania;
public NotificacionPromocion(CanalEnvio canal, Campania campania) {
super(canal);
this.campania = campania;
}
@Override
public void notificar(Pedido pedido) {
Cliente cliente = pedido.getCliente();
if (!cliente.haDadoConsentimientoMarketing()) {
return; // la regla legal, UNA sola vez
}
canal.enviar(cliente, campania.getTitulo(),
campania.getTexto() + "\n" + campania.getCondicionesLegales());
}
}El cliente cruza el puente al construir: cualquier tipo con cualquier canal, decidido en ejecución (por preferencias del usuario, por criticidad del mensaje, por coste del canal):
CanalEnvio canalPreferido = cliente.prefiereSms() ? new CanalSms() : new CanalPush();
new NotificacionConfirmacion(canalPreferido).notificar(pedido);
new NotificacionRetraso(new CanalSms(), 25).notificar(pedido); // retrasos: siempre SMS
new NotificacionPromocion(new CanalEmail(), campania).notificar(pedido); // promos: email, más baratoLas 9 combinaciones existen y funcionan, pero ninguna tiene clase propia: son composiciones armadas al vuelo. Y la creación de los canales no tiene por qué ser un new a la vista: el RegistroNotificadores del módulo 2 puede reconvertirse en registro de Supplier<CanalEnvio> y seguir siendo el punto único donde se decide el canal — los patrones creacionales fabrican las puntas del puente; Bridge decide su forma.
Crecer por los dos ejes
La prueba de fuego de la separación es el coste de crecer:
- Tipo nuevo ("valora tu pedido"): una clase,
NotificacionValoracion extends Notificacion. Los canales ni se recompilan. - Canal nuevo (WhatsApp): una clase,
CanalWhatsApp implements CanalEnvio. Los tipos ni se enteran, y automáticamente todos los tipos existentes saben salir por WhatsApp.
Cada eje cumple OCP por su cuenta: extensión sin modificación, en los dos sentidos. Compáralo con la jerarquía multiplicativa, donde el canal nuevo costaba una clase por cada tipo y esparcía la lógica duplicada un poco más.
Una decisión de diseño que verás en implementaciones reales: la interfaz del implementor no tiene que imitar a la de la abstracción. CanalEnvio.enviar(destinatario, titulo, cuerpo) es más primitiva que Notificacion.notificar(pedido): la abstracción trabaja en términos de negocio (pedidos, campañas, consentimientos) y traduce a los términos técnicos del canal (título, cuerpo, destinatario). Esa asimetría es sana; si ambas interfaces fueran idénticas, sospecharías con razón que una de las dos capas no aporta nada.
Bridge frente a Adapter (y una mención a Strategy)
Bridge y Adapter se confunden porque el dibujo local es parecido: un objeto que delega en otro tras una interfaz. La diferencia está en el cuándo y el porqué, y es tajante:
| Aspecto | Adapter | Bridge |
|---|---|---|
| Momento | A posteriori: las piezas ya existen y no encajan | A priori: se diseña antes de que las jerarquías crezcan |
| Problema | Interfaces incompatibles que no controlas | Una jerarquía que amenaza con crecer por producto de dos ejes |
| Las interfaces implicadas | Ajena (adaptee) y propia (target): se traducen | Ambas propias: se diseñan para colaborar |
| Sabor | Arreglo, aduana, parche honesto | Arquitectura, planificación de la variación |
| En PideYa | AdaptadorPayPal para un SDK heredado |
Notificacion × CanalEnvio diseñado por nosotros |
Una frase para llevarse: Adapter hace que cosas que ya existen encajen; Bridge evita que cosas que van a existir se suelden. De hecho conviven bien: si mañana WhatsApp solo ofrece un SDK con interfaz alienígena, CanalWhatsApp será internamente un Adapter de ese SDK... colgado del puente como un implementor más.
Strategy (solo mención, se desarrolla en la lección 04-10): estructuralmente, el puente hacia CanalEnvio es casi idéntico a una estrategia intercambiable. La diferencia volverá a ser de intención —Strategy intercambia algoritmos dentro de un objeto; Bridge separa jerarquías enteras que crecen por su cuenta— y la afinaremos allí. Por ahora, quédate con que si solo hubiera un tipo de notificación y tres formas de enviarla, aquello sería más Strategy que Bridge; el puente se justifica cuando los dos lados tienen jerarquía.
Cuándo usarlo y cuándo no
Úsalo cuando:
- Detectas (o prevés con evidencia) dos dimensiones de variación independientes en una misma responsabilidad: qué/cómo, negocio/plataforma, modelo/render, operación/persistencia.
- Las subclases empiezan a llamarse
AlgoConAlgo(ConfirmacionPush,InformePdfMensual): el nombre compuesto delata los dos ejes fundidos. - Quieres poder cambiar la implementación en ejecución (canal según preferencias del cliente) o repartir el desarrollo de cada jerarquía a equipos distintos.
No lo uses cuando:
- Solo varía un eje: una jerarquía normal (o una Strategy) basta, y el puente sería una indirección gratuita — la sobreingeniería de la lección de la balanza.
- La "segunda dimensión" es especulativa: "quizá algún día haya más canales" sin ningún encargo real es YAGNI de manual. El síntoma honesto es la explosión ya iniciada (esas 9 clases) o el encargo firmado del cuarto canal.
- Las dos jerarquías no son de verdad independientes: si cada tipo de notificación necesita conocer detalles íntimos de cada canal, el puente se convierte en un colador de
instanceofy es mejor replantear el reparto de responsabilidades.
Relación con otros patrones (solo mención): Abstract Factory puede crear pares abstracción-implementor coherentes; los implementors concretos pueden ser Adapters de SDKs externos; Decorator puede envolver un CanalEnvio para añadirle reintentos o logging sin tocar el puente; y la comparación fina con Strategy y State llegará en el módulo 4.
Errores Comunes y Consejos
- Abstracción que puentea... y luego pregunta. Si
NotificacionRetrasohaceif (canal instanceof CanalSms)para acortar el texto, el puente está agujereado: esa decisión (limitar caracteres) pertenece al canal. Todo conocimiento técnico, al lado técnico. - Implementor gordo. Una interfaz
CanalEnviocon 15 métodos (adjuntos, HTML, acuses de recibo, prioridades...) obliga a cada canal a implementar cosas que no soporta. Mantén el implementor primitivo y deja la riqueza en la abstracción; si un subconjunto de canales comparte capacidades extra, segrega interfaces (ISP, lección 01-03). - Confundir los lados. Poner las reglas de consentimiento en
CanalEmail"porque las promos van por email". El día que las promos salgan también por push, la regla legal está en el sitio equivocado. Negocio en la abstracción, técnica en el implementor, siempre. - Bridge póstumo. Introducir el puente cuando ya hay 20 clases combinatorias es más caro que hacerlo con 6, pero sigue siendo rentable; hazlo por pasos (aparece
CanalEnvio, cadaTipoCanaldelega, se funden los duplicados) y con tests de por medio. Lo que no es rentable es introducirlo cuando no hay ni habrá segundo eje. - Consejo: el test de fuego para ubicar un código dudoso es preguntar "¿esto cambia si cambia el canal, o si cambia el negocio?". El umbral del cupón no cambia por usar WhatsApp: abstracción. El límite de 160 caracteres no cambia por ser una promo: implementor.
Ejercicios
Ejercicio 1: notificaciones también para repartidores
PideYa quiere notificar también a los repartidores (nueva asignación de pedido, cambio de zona), y los repartidores solo reciben push o SMS. Indica qué clases nuevas necesitas y cuáles existentes se modifican. Pista: ¿el destinatario es un eje nuevo o cabe en los existentes?
Ejercicio 2: detectar el puente roto
¿Qué está mal aquí y cómo lo arreglarías?
public class CanalEmail implements CanalEnvio {
@Override
public void enviar(Cliente destinatario, String titulo, String cuerpo) {
if (titulo.startsWith("Pedido") && titulo.contains("confirmado")) {
cuerpo += "\nGracias por confiar en PideYa."; // firma solo en confirmaciones
}
servidorSmtp.mandar(destinatario.getEmail(), titulo, plantillaHtml(cuerpo));
}
}Ejercicio 3: contar clases
El equipo de informes tiene InformeVentasPdf, InformeVentasExcel, InformeRepartosPdf, InformeRepartosExcel, InformeClientesPdf e InformeClientesExcel, y llegan encargos de un formato HTML y un informe de promociones. (a) ¿Cuántas clases habrá sin Bridge cuando se completen ambos encargos? (b) ¿Y con Bridge (cuenta jerarquías, base e interfaz)? (c) Nombra los roles GoF resultantes.
Soluciones
Solución 1: no hace falta ningún eje nuevo; hace falta generalizar el destinatario. Cambios: (1) CanalEnvio.enviar pasa a recibir un tipo común (Destinatario, interfaz que implementan Cliente y Repartidor, con lo que cada canal necesita: teléfono, tokens, email) — única modificación a existentes, mecánica. (2) Clases nuevas, todas refinamientos de la abstracción: NotificacionAsignacion extends Notificacion y NotificacionCambioZona extends Notificacion. Los canales concretos no se tocan (push y SMS ya existen); que los repartidores no usen email es una regla de quién compone, no una clase nueva: simplemente nunca se construye una notificación de repartidor con CanalEmail (y puede blindarse en la fábrica que compone). Total: 2 clases nuevas + 1 interfaz de destinatario. Con la jerarquía multiplicativa habrían sido 4 clases nuevas y contando.
Solución 2: el canal está husmeando el contenido para inferir el tipo de negocio (parsear el título para saber si es una confirmación): conocimiento del lado del negocio infiltrado en el lado técnico, y además frágil (se rompe al traducir el título o cambiar el texto). Arreglo: la firma "Gracias por confiar en PideYa" es una decisión del tipo de notificación → se añade al cuerpo en NotificacionConfirmacion.notificar(...), y CanalEmail vuelve a ser tonto. Si lo que se quiere es una firma en todos los emails (decisión sí técnica del canal), entonces se añade incondicionalmente en el canal, sin mirar el título.
Solución 3: (a) Sin Bridge: ejes 4 informes × 3 formatos = 12 clases combinatorias. (b) Con Bridge: 4 refinamientos (InformeVentas, InformeRepartos, InformeClientes, InformePromociones) + 3 implementors (FormatoPdf, FormatoExcel, FormatoHtml) + base Informe + interfaz FormatoSalida = 9 clases/interfaces, y sobre todo crecimiento futuro aditivo, no multiplicativo. (c) Roles: Abstraction Informe; RefinedAbstraction los cuatro informes; Implementor FormatoSalida; ConcreteImplementor los tres formatos.
Conclusión
Bridge parte en dos una jerarquía que crecía por producto: a un lado la abstracción (los tipos de notificación, con todo el negocio), al otro la implementación (los canales, con toda la técnica), y entre ambos una referencia de composición que se cruza en ejecución. El resultado es crecimiento aditivo en los dos ejes, reglas escritas una sola vez y equipos que pueden evolucionar cada lado sin pisarse. Y quedó trazada la frontera con Adapter —diseño a priori frente a arreglo a posteriori— que volverá en la comparativa final.
Hasta ahora hemos conectado piezas de dos en dos: adaptador y adaptee, abstracción e implementor. Pero hay estructuras en PideYa que no son parejas sino árboles: la carta de un restaurante tiene secciones, dentro subsecciones, dentro platos... y queremos preguntarle el precio o la disponibilidad a cualquier nodo sin preguntarnos primero qué es. Tratar al grupo igual que al individuo: esa uniformidad recursiva es el próximo patrón. Nos vemos en Composite.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
