Patrón Mediator para Componentes UI Desacoplados
Reduce dependencias caóticas entre componentes UI introduciendo un mediador que centraliza la comunicación y evita referencias explícitas entre pares
Descripción general
El patrón Mediator define un objeto que encapsula cómo interactúa un conjunto de objetos. En lugar de que los componentes se referencien entre sí directamente, se dirigen a un mediador, reduciendo las conexiones explícitas de many-to-many a many-to-one. Este recurso lo aplica a un dashboard en TypeScript —filtros, gráficos, tablas y mapas que deben mantenerse sincronizados. Para la versión general, consulta el patrón Mediator.
Cuándo usarlo
- Los componentes tienen relaciones many-to-many que de otro modo crearían acoplamiento fuerte
- Reusar un componente de forma independiente es difícil porque depende de pares concretos
- La lógica de comunicación está dispersa entre componentes y es difícil de probar
- Quieres un único punto para enrutar, filtrar o registrar cada interacción
Evítalo cuando:
- Dos o tres widgets comparten un padre sencillo —elevar el estado o un callback por props basta, y un mediador es pura ceremonia
- Las interacciones son notificaciones de un solo sentido sin lógica de respuesta —un Observer plano cubre eso
- Ya usas un state manager; añadir un mediador casero junto a Redux o Zustand reparte tu cableado entre dos tablas de enrutado en lugar de una
- El equipo no se compromete a mantener el mediador delgado —un god object es peor que el acoplamiento que reemplazó
Problema
Un dashboard con filtros, gráficos, tablas y mapas exige que cada widget notifique a todos los demás cuando cambian los datos. Cada widget guarda referencias a otros 5-6 —una pesadilla de dependencias donde añadir un widget nuevo obliga a tocar todos los existentes, y probar uno de forma aislada exige mockear todo su vecindario.
Solución
Cada colega envía eventos al mediador; el mediador decide quién reacciona. Los widgets no se conocen entre sí —la red de referencias N×(N-1) se convierte en N conexiones hacia un único hub.
// mediator/DashboardMediator.ts
interface Mediator {
notify(sender: Component, event: string, data?: unknown): void;
}
abstract class Component {
constructor(protected mediator: Mediator) {}
send(event: string, data?: unknown): void {
this.mediator.notify(this, event, data);
}
}
class FilterPanel extends Component {
private selectedRegion = 'all';
selectRegion(region: string): void {
this.selectedRegion = region;
this.send('region-changed', region);
}
}
class ChartWidget extends Component {
private data: unknown[] = [];
updateData(data: unknown[]): void {
this.data = data;
this.render();
}
private render(): void {
console.log('Chart rendered with', this.data.length, 'points');
}
}
class TableWidget extends Component {
private rows: unknown[] = [];
updateRows(rows: unknown[]): void {
this.rows = rows;
console.log('Table updated with', rows.length, 'rows');
}
}
class MapWidget extends Component {
private center = { lat: 0, lng: 0 };
panTo(center: { lat: number; lng: number }): void {
this.center = center;
console.log('Map centered at', center);
}
}
// El mediador orquesta toda la comunicación
class DashboardMediator implements Mediator {
private filters: FilterPanel;
private chart: ChartWidget;
private table: TableWidget;
private map: MapWidget;
setComponents(
filters: FilterPanel,
chart: ChartWidget,
table: TableWidget,
map: MapWidget
): void {
this.filters = filters;
this.chart = chart;
this.table = table;
this.map = map;
}
notify(sender: Component, event: string, data?: unknown): void {
switch (event) {
case 'region-changed': {
const filteredData = this.fetchDataForRegion(data as string);
this.chart.updateData(filteredData);
this.table.updateRows(filteredData);
this.map.panTo(this.getRegionCenter(data as string));
break;
}
case 'chart-point-clicked': {
const point = data as { lat: number; lng: number };
this.map.panTo(point);
break;
}
}
}
private fetchDataForRegion(region: string): unknown[] {
return [{ id: 1, region }];
}
private getRegionCenter(region: string): { lat: number; lng: number } {
const centers: Record<string, { lat: number; lng: number }> = {
'north': { lat: 45, lng: 0 },
'south': { lat: -45, lng: 0 },
};
return centers[region] || { lat: 0, lng: 0 };
}
}
// Uso
const mediator = new DashboardMediator();
const filters = new FilterPanel(mediator);
const chart = new ChartWidget(mediator);
const table = new TableWidget(mediator);
const map = new MapWidget(mediator);
mediator.setComponents(filters, chart, table, map);
filters.selectRegion('north');
- Código complementario: el repositorio companion de mediator-pattern-components empaqueta estos ejemplos como archivos TypeScript ejecutables.
Variación: Event Bus Mediator
Una variante más suelta del mediador: los componentes se suscriben a eventos nombrados en vez de que el mediador conozca cada widget por su nombre. Útil cuando la tabla de enrutado cambia en runtime.
// mediator/EventBus.ts
class EventBus {
private listeners = new Map<string, Set<(data: unknown) => void>>();
subscribe(event: string, callback: (data: unknown) => void): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => this.listeners.get(event)?.delete(callback);
}
emit(event: string, data?: unknown): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
const bus = new EventBus();
bus.subscribe('user-login', user => console.log('Logged in:', user));
bus.emit('user-login', { id: 1 });
Fíjate en que subscribe devuelve una función de desuscripción —es responsabilidad del llamador ejecutarla, o aparece el clásico memory leak de listeners zombie. A diferencia de DashboardMediator, el bus no comprueba tipos ni enruta; cualquier componente puede emitir cualquier evento, lo que da flexibilidad a costa de descubribilidad —no puedes saber quién reacciona a qué sin buscar el string.
Cómo funciona
El patrón tiene cuatro roles:
- Mediator (interfaz
Mediator) declara la interfaz de comunicación —un único punto de entradanotify. - Concrete Mediator (
DashboardMediator) alberga la lógica de coordinación: qué colegas reaccionan a qué eventos. - Colleague (
FilterPanel,ChartWidget…) envía eventos al mediador en lugar de llamar directamente a sus pares. - Client conecta el mediador con todos los colegas, normalmente en el composition root.
Añadir un quinto widget significa registrarlo en el mediador —los cuatro existentes nunca se enteran de que existe.
Escenario: Chat Room
La misma forma sirve para un chat —los usuarios no se conocen, la sala enruta cada mensaje:
// Patrón Mediator: centralizar la comunicación entre componentes
interface ChatMediator {
sendMessage(msg: string, user: User): void;
registerUser(user: User): void;
}
abstract class User {
constructor(protected mediator: ChatMediator, public name: string) {}
abstract send(msg: string): void;
abstract receive(msg: string): void;
}
class ChatUser extends User {
send(msg: string) { this.mediator.sendMessage(msg, this); }
receive(msg: string) { console.log(`[${this.name}] received: ${msg}`); }
}
class ChatRoom implements ChatMediator {
private users: User[] = [];
registerUser(user: User) { this.users.push(user); }
sendMessage(msg: string, sender: User) {
// Emitir a todos excepto al remitente
this.users.filter(u => u !== sender).forEach(u => u.receive(msg));
}
}
const chat = new ChatRoom();
const alice = new ChatUser(chat, "Alice");
const bob = new ChatUser(chat, "Bob");
chat.registerUser(alice);
chat.registerUser(bob);
alice.send("Hola a todos");
// [Bob] received: Hola a todos
Lecciones: la sala puede filtrar, transformar o registrar mensajes sin que los usuarios lo sepan; registrar un usuario nuevo no cambia nada más; y el acoplamiento baja de N×(N-1) a N —cada participante solo nombra al mediador.
Compromisos
- El mediador puede volverse un god object: si metes todas las reglas de enrutado en una clase, reconstruyes el lío del que escapabas —divídelo por dominio cuando crezca.
- Eventos stringly-typed:
event: stringescala mal; prefiere una unión de literales o un enum para que el compilador detecte erratas. - Interacciones ocultas: leer un componente ya no dice qué dispara —el cableado vive en el mediador y la navegación se vuelve indirecta.
- Depurabilidad: una única tabla de enrutado es también un único punto donde poner breakpoints y logs —una ventaja real cuando las interacciones se complican.
Errores comunes
- Crear un mediador tan grande que se vuelve inmantenible —la misma trampa del controlador monolítico que pretendía sustituir.
- Saltarse el mediador para comunicación directa entre componentes “solo esta vez”, reintroduciendo el acoplamiento sin ruido.
- No desuscribir los listeners de eventos, provocando memory leaks —por eso
subscribedevuelve una función de limpieza. - Confundirlo con Observer: Observer emite one-to-many y el sujeto ignora quién escucha; Mediator enruta many-to-many y conoce a cada colega.
Lecturas recomendadas
Preguntas frecuentes
¿En qué se diferencia de Observer?
Observer es broadcast one-to-many —el sujeto dispara y le da igual quién escucha. Mediator es enrutado many-to-many a través de un coordinador que conoce a todos los colegas y decide quién reacciona.
¿Cuándo debería usar un state manager en su lugar?
Cuando la necesidad principal es estado compartido, no solo comunicación. Mediator enruta mensajes; librerías como Redux o Zustand gestionan datos. En la práctica los state managers son mediadores evolucionados —elígelos cuando las reglas de sincronización superen un bus hecho a mano.
¿Sirve este patrón para proyectos pequeños?
Normalmente no. Dos o tres widgets hablando por un componente padre o un callback no necesitan un hub —el patrón compensa cuando la matriz de interacciones duele de rastrear.
¿Qué relación tiene Mediator con CQRS?
Los buses de CQRS son mediadores aplicados a comandos y consultas —los handlers se registran para un tipo de mensaje y el bus lo enruta. Mismo coordinador central, otro dominio.
¿Puedo aplicar el patrón parcialmente?
Sí. Enruta por el mediador solo las interacciones enredadas —sincronización del dashboard, pasos de un wizard— y deja los props padre-hijo simples como están. El patrón admite adopción acotada.
Recursos Relacionados
Patrón Mediator
Define un objeto que encapsula cómo interactúa un conjunto de objetos. Un patrón de comportamiento para reducir dependencias caóticas.
PatternPatrón Observer
Define un mecanismo de suscripción para notificar a múltiples objetos sobre eventos. Patrón de diseño conductual para comunicación basada en eventos.
PatternPatrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.
PatternComposite Pattern para Arboles de Componentes UI en React
Usa el Composite pattern para componer objetos en estructuras de arbol, permitiendo que clientes traten objetos individuales y composiciones uniformemente en jerarquias de componentes
PatternPatrón Bridge para Desacoplar Componentes UI de Temas
Separa la abstracción de su implementación para que ambas varien de forma independiente: el patrón Bridge aplicado a temas UI y motores de renderizado.
PatternPatrón Backend for Frontend (BFF)
Dale a cada tipo de cliente su propio backend, que agrega los servicios de dominio y adapta la forma de los datos a cada plataforma.