La logística de PideYa es una conversación a tres bandas que no para: la cocina termina un pedido y busca repartidor; el repartidor acaba una entrega y pregunta si hay algo pendiente; un pedido lleva demasiado tiempo esperando y hay que reasignarlo. En el código actual, cocina, pedidos y repartidores se conocen y se llaman entre sí: una malla donde cada clase guarda referencias a las demás, y donde añadir una pieza (¡llegan los repartidores en moto con nevera!) obliga a tocarlo todo. Mediator propone rehacer la topología: que ningún colega hable con otro directamente y toda la coordinación pase por un objeto central — convertir la malla en una estrella. Y con la misma honestidad de siempre: veremos también cómo esa estrella puede degenerar en el temido mediador-dios.
Contenido
- El problema en PideYa: la malla del reparto
- Malla vs. estrella: la cuenta del acoplamiento
- Intención y estructura del patrón
- Implementación Java completa:
CentralReparto - La conversación en el tiempo
- Variantes del patrón
- El riesgo del mediador-dios
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: la malla del reparto
El estado actual, resumido en tres clases que se abrazan:
public class Cocina {
private List<Repartidor> repartidores; // conoce a los repartidores
public void pedidoTerminado(Pedido pedido) {
for (Repartidor r : repartidores) { // y los recorre ella misma
if (r.estaDisponible() && r.aceptaZona(pedido.getZona())) {
r.asignar(pedido);
return;
}
}
pedido.marcarEsperandoRepartidor(); // y decide qué pasa si no hay
}
}
public class Repartidor {
private Cocina cocina; // conoce a la cocina
public void entregaCompletada() {
this.disponible = true;
Pedido pendiente = cocina.algunPedidoEsperando(this.zona); // y le pregunta
if (pendiente != null) {
asignar(pendiente);
}
}
}Los síntomas, uno a uno:
- Todos conocen a todos:
Cocinaguarda repartidores,Repartidorguarda la cocina, y el monitor de pedidos atascados conocerá a ambos. Cada referencia cruzada es una dependencia de compilación y unnew/setter que alguien mantiene. - La lógica de coordinación está repartida: parte de "cómo se asigna un repartidor" vive en
Cocina, parte enRepartidor. Para entender el protocolo completo hay que leer todas las clases; para cambiarlo, tocarlas todas. - Reutilización imposible: un
Repartidorno puede probarse ni reutilizarse sin unaCocinade verdad colgando de él. - Crecer duele cuadráticamente: añadir el monitor de atascos, o repartidores externos de una subcontrata, significa nuevas referencias cruzadas en las clases existentes.
Y nótese lo que no es el problema: cada clase hace bien su trabajo individual (cocinar, repartir). Lo que sobra es que además carguen con el protocolo de coordinación. Ese protocolo pide un dueño único.
Malla vs. estrella: la cuenta del acoplamiento
Con n participantes que colaboran todos con todos, la malla tiene hasta n(n−1)/2 conexiones; la estrella, exactamente n. Con 4 colegas: 6 frente a 4; con 8: 28 frente a 8. Pero más importante que la cuenta es quién sabe qué: en la estrella, cada colega solo conoce al mediador — añadir el quinto colega no toca a los otros cuatro.
flowchart LR
subgraph Malla["Malla: todos conocen a todos"]
C1[Cocina] --- R1[Repartidores]
C1 --- P1[Pedidos listos]
C1 --- M1[Monitor atascos]
R1 --- P1
R1 --- M1
P1 --- M1
end
subgraph Estrella["Estrella: todos conocen al mediador"]
C2[Cocina] --- X[CentralReparto]
R2[Repartidores] --- X
P2[Pedidos listos] --- X
M2[Monitor atascos] --- X
end
Intención y estructura del patrón
Intención (GoF): definir un objeto que encapsula cómo interactúa un conjunto de objetos. Mediator promueve el bajo acoplamiento evitando que los objetos se refieran unos a otros explícitamente, y permite variar su interacción de forma independiente.
classDiagram
class MediadorReparto {
<<interface>>
+pedidoListo(pedido: Pedido)
+repartidorDisponible(r: Repartidor)
+pedidoAtascado(pedido: Pedido)
}
class CentralReparto {
-enEspera: Queue~Pedido~
-disponibles: List~Repartidor~
+pedidoListo(pedido: Pedido)
+repartidorDisponible(r: Repartidor)
+pedidoAtascado(pedido: Pedido)
}
class Cocina {
-central: MediadorReparto
+terminarPedido(p: Pedido)
}
class Repartidor {
-central: MediadorReparto
+entregaCompletada()
+asignar(p: Pedido)
}
MediadorReparto <|.. CentralReparto
Cocina --> MediadorReparto : avisa a
Repartidor --> MediadorReparto : avisa a
CentralReparto --> Repartidor : coordina
CentralReparto ..> Cocina : coordina
| Rol GoF | En PideYa |
|---|---|
| Mediator (interfaz de coordinación) | MediadorReparto |
| ConcreteMediator (el protocolo entero) | CentralReparto |
| Colleague (participantes que solo conocen al mediador) | Cocina, Repartidor (y mañana MonitorAtascos, RepartidorExterno...) |
La regla de oro que define al patrón: los colegas nunca se referencian entre sí. Un colega hace su trabajo local y, cuando pasa algo relevante para otros, se lo cuenta al mediador; el mediador decide a quién toca reaccionar y se lo ordena.
Implementación Java completa: CentralReparto
La interfaz del mediador. Sus métodos son los acontecimientos que los colegas comunican — nómbralos por lo que pasó, no por lo que hay que hacer (eso lo decide el mediador):
public interface MediadorReparto {
void pedidoListo(Pedido pedido);
void repartidorDisponible(Repartidor repartidor);
void pedidoAtascado(Pedido pedido);
}Los colegas, ahora ligeros: hacen lo suyo y avisan. Repartidor ya no conoce a Cocina; Cocina ya no recorre repartidores:
public class Cocina {
private final MediadorReparto central;
public Cocina(MediadorReparto central) { // inyectado, como siempre (DIP)
this.central = central;
}
public void terminarPedido(Pedido pedido) {
pedido.marcarListoParaRecoger(); // trabajo propio
central.pedidoListo(pedido); // aviso al mediador; fin
}
}
public class Repartidor {
private final MediadorReparto central;
private final String zona;
private boolean disponible = true;
public Repartidor(MediadorReparto central, String zona) {
this.central = central;
this.zona = zona;
}
public void entregaCompletada() {
this.disponible = true; // trabajo propio
central.repartidorDisponible(this); // aviso; ni idea de qué pasará
}
/** Orden que recibe DEL mediador. */
public void asignar(Pedido pedido) {
this.disponible = false;
this.pedidoActual = pedido;
// recoger, navegar, etc.
}
public boolean estaDisponible() { return disponible; }
public boolean aceptaZona(String z) { return zona.equals(z); }
}El mediador concreto: aquí, y solo aquí, vive el protocolo completo de coordinación — legible de arriba abajo en una clase:
public class CentralReparto implements MediadorReparto {
private final Queue<Pedido> enEspera = new ArrayDeque<>();
private final List<Repartidor> flota = new ArrayList<>();
public void registrarRepartidor(Repartidor r) {
flota.add(r);
}
@Override
public void pedidoListo(Pedido pedido) {
Optional<Repartidor> candidato = buscarDisponible(pedido.getZona());
if (candidato.isPresent()) {
candidato.get().asignar(pedido);
} else {
enEspera.add(pedido); // el protocolo decide qué hacer
RegistroEventos.INSTANCIA.info("Pedido " + pedido.getId() + " en espera");
}
}
@Override
public void repartidorDisponible(Repartidor repartidor) {
enEspera.stream()
.filter(p -> repartidor.aceptaZona(p.getZona()))
.findFirst()
.ifPresent(p -> {
enEspera.remove(p);
repartidor.asignar(p);
});
}
@Override
public void pedidoAtascado(Pedido pedido) {
// reasignación: volver a la cola con prioridad, avisar a soporte...
enEspera.add(pedido);
}
private Optional<Repartidor> buscarDisponible(String zona) {
return flota.stream()
.filter(Repartidor::estaDisponible)
.filter(r -> r.aceptaZona(zona))
.findFirst();
}
}Repara en lo ganado:
- El protocolo es un solo texto: "si hay repartidor en zona, asignar; si no, encolar; cuando alguien se libere, mirar la cola". Antes estaba esparcido en tres clases.
- Cambiar el criterio de asignación ("el más cercano" en vez de "el primero disponible") toca una línea de una clase. De hecho, ese criterio es una familia de algoritmos intercambiables... exactamente lo que Strategy reificará en su lección — el mediador será su cliente perfecto.
- Testear
Repartidorahora es trivial: se le inyecta unMediadorRepartofalso y punto. - Añadir un colega nuevo (el
MonitorAtascosque detecta esperas largas y llama apedidoAtascado) no toca ni aCocinani aRepartidor.
La conversación en el tiempo
El mismo escenario del principio — un repartidor se libera y hay un pedido esperando — ahora en estrella:
sequenceDiagram
participant K as Cocina
participant C as CentralReparto
participant R as Repartidor (María)
K->>C: pedidoListo(pedido 88)
C->>C: ¿disponible en zona? no → encolar
Note over R: María termina otra entrega
R->>C: repartidorDisponible(María)
C->>C: ¿algo en espera para su zona? sí: el 88
C->>R: asignar(pedido 88)
Note over K,R: Cocina y María jamás se hablaron
Variantes del patrón
- Con o sin interfaz Mediator: si solo existirá una central, GoF admite saltarse la interfaz y usar la clase concreta. Nosotros la mantenemos por testabilidad (mediadores falsos en los tests de los colegas) y porque una
CentralRepartoExterna(subcontrata) es plausible. - ¿Cómo avisa el colega al mediador? Nuestra versión usa métodos específicos (
pedidoListo(...)), clara y tipada. La alternativa es un método único genérico (notificar(colega, "PEDIDO_LISTO", datos)) — más flexible, menos segura; en cuanto lo veas, estás a un paso del bus de eventos (mención en la puerta de Observer y desarrollo en el módulo 6). - Mediador de UI: el ejemplo canónico del GoF es un diálogo cuyos widgets (botón, lista, campo de texto) se coordinan a través del diálogo — "al marcar la casilla se habilita el botón". Si has escrito un controlador de pantalla que coordina sus componentes, has escrito un mediador.
El riesgo del mediador-dios
La crítica clásica al patrón, y es justa: el mediador concentra el acoplamiento en lugar de eliminarlo. Eso es bueno (está localizado, legible, cambiable) hasta que deja de serlo: la central que además calcula rutas, aplica bonificaciones, decide turnos y envía notificaciones se convierte en un objeto-dios — la mayor mancha de responsabilidad del sistema, intocable por miedo y cuello de botella de todos los cambios. Señales de alarma y antídotos:
- El mediador hace trabajo de dominio en vez de coordinar → extrae ese trabajo a los colegas o a servicios (SRP: el mediador coordina, los colegas trabajan).
- El mediador crece con métodos que solo interesan a una pareja de colegas → quizá esa pareja merece hablarse directamente o tener su propio mediador pequeño.
- Los algoritmos dentro del mediador varían → extráelos como estrategias (Strategy) y deja al mediador la orquestación.
La versión honesta del contrato: Mediator no reduce la complejidad del protocolo — la muda a un único lugar. Si el protocolo es intrínsecamente enorme, el mediador será grande; el patrón te debe orden, no milagros.
Cuándo usarlo y cuándo no
Úsalo cuando:
- Un conjunto de objetos colabora de formas complejas y cambiantes, y las referencias cruzadas se multiplican.
- Quieres reutilizar o testear los colegas sueltos, sin arrastrar a sus interlocutores.
- El protocolo de coordinación merece ser una pieza con nombre propio, legible y modificable de una vez.
Evítalo cuando:
- Solo hay dos colaboradores con una relación simple: una llamada directa (o un Observer) basta; un mediador entre dos es burocracia.
- El "protocolo" es trivial y estable: la malla de dos hilos no duele.
- Estarías creando el mediador-dios desde el día uno: si la clase nace con veinte responsabilidades, el problema es de reparto de dominio, no de topología.
Relación con otros patrones
- Observer: la otra forma de desacoplar comunicación — y la confusión más frecuente del módulo. Adelanto de una línea (careo completo en la comparativa): Observer difunde "ha pasado X" a quien quiera oírlo, sin protocolo central; Mediator dirige un protocolo con papeles conocidos. Además, un mediador puede implementarse escuchando a sus colegas como observador.
- Facade: parecido superficial (un objeto delante de varios). La fachada ofrece una interfaz unidireccional simplificada hacia un subsistema que no la conoce; el mediador mantiene un diálogo bidireccional con colegas que sí lo conocen.
- Strategy: los criterios variables del mediador (asignación de repartidor) se extraen como estrategias.
- Singleton: la tentación de hacer
CentralRepartosingleton global; mejor inyectarla, por los motivos que ya sufrimos en el módulo 2.
Errores comunes
- Colegas que se guardan referencias entre sí "solo para este caso": la primera referencia cruzada reabre la malla; a la tercera, tienes malla y mediador (lo peor de ambos mundos).
- El mediador que trabaja: si
CentralRepartocalcula la ruta o cobra la entrega, ha dejado de coordinar. Coordinar es decidir quién y cuándo; el cómo es de los colegas. - Ciclos de notificación: el mediador ordena a un colega, el colega notifica al mediador, que vuelve a ordenar... Protégete distinguiendo "aviso de acontecimiento" (colega → mediador) de "orden" (mediador → colega) y no notifiques acontecimientos desde las órdenes recibidas.
- Interfaz de mediador que calca una implementación: métodos como
asignarAlPrimerDisponible(...)fijan el protocolo en la interfaz; nómbralos por acontecimientos (pedidoListo) para poder cambiar la política sin tocar a los colegas. - Convertirlo en cajón de sastre: "ya que todo pasa por aquí, meto también el logging, la caché y las métricas". Cada inquilino nuevo acerca el mediador-dios.
Ejercicios
Ejercicio 1: nuevo colega sin tocar los viejos
Implementa MonitorAtascos: cada minuto revisa los pedidos en espera y, si alguno lleva más de 10 minutos, llama a central.pedidoAtascado(pedido). Además, la central debe entonces notificar a soporte con el Notificador del módulo 2. Indica qué clases se crean, cuáles se modifican y cuáles quedan intactas.
Ejercicio 2: malla → estrella
Dibuja (o describe) las dependencias de estas cuatro clases antes y después de aplicar Mediator: Cocina, Repartidor, PantallaSeguimiento (muestra al cliente dónde está su pedido) y CentralReparto. ¿Cuántas referencias entre clases hay en cada versión?
Ejercicio 3: detectar al dios
Un año después, CentralReparto tiene 1.800 líneas: asigna repartidores, calcula rutas con tráfico, aplica el bono por lluvia, gestiona los turnos y envía las notificaciones push. Propón un reparto: ¿qué se queda en el mediador y a dónde va cada cosa? (Nombra patrones ya vistos donde apliquen.)
Soluciones
Solución 1: se crea MonitorAtascos (colega nuevo que solo conoce MediadorReparto); se modifica solo CentralReparto (el cuerpo de pedidoAtascado añade notificadorSoporte.enviar(...), con el notificador inyectado en su constructor); quedan intactas Cocina, Repartidor y la interfaz MediadorReparto (el método ya existía). Ese es el dividendo de la estrella: crecer sin tocar a los vecinos.
Solución 2: antes (malla): Cocina→Repartidor, Repartidor→Cocina, PantallaSeguimiento→Cocina, PantallaSeguimiento→Repartidor — 4 referencias (y creciendo cuadráticamente con cada colega). Después (estrella): Cocina→Mediador, Repartidor→Mediador, PantallaSeguimiento→Mediador, y el mediador conoce a sus colegas — la coordinación completa queda en 1 clase y cada colega tiene exactamente 1 dependencia, testeable con un mediador falso.
Solución 3: se queda en CentralReparto únicamente la orquestación: recibir acontecimientos, consultar piezas y ordenar asignaciones. Se van: el cálculo de rutas a un ServicioRutas (colega/servicio de dominio); el criterio de asignación y el bono por lluvia a estrategias (Strategy) intercambiables que la central usa; la gestión de turnos a un GestorTurnos propio (la central le pregunta disponibilidad); las notificaciones push a los Notificador existentes, invocados por la central pero implementados fuera — o mejor aún, disparadas por los cambios de estado del pedido vía Observer, que es justo la lección que sigue tras la próxima.
Conclusión
Mediator reordena la topología de la colaboración: de la malla donde cocina, pedidos y repartidores se conocían todos con todos, a la estrella donde cada colega solo conoce a CentralReparto y el protocolo de coordinación vive entero, legible y cambiable, en un único lugar. Has visto la cuenta del acoplamiento (n frente a n²), la conversación en secuencia, sus variantes, y la advertencia que lo acompaña siempre: el mediador concentra el acoplamiento — vigila que no engorde hasta mediador-dios, extrayendo trabajo a colegas y algoritmos a estrategias.
El siguiente patrón cambia de escenario pero no de familia: del reparto al carrito del cliente. Editar el carrito es ir probando —añado esto, quito aquello, aplico un cupón— y el cliente quiere poder volver atrás sin que el carrito exponga sus tripas para que alguien las fotografíe. Guardar y restaurar estado sin romper la encapsulación: ese equilibrio delicado es exactamente la especialidad de Memento.
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
