El patrón Mediator centraliza la comunicación entre objetos, evitando que los colegas se conozcan directamente. En lugar de referencias cruzadas y notificaciones en cascada, cada componente interactúa con un mediador que coordina las acciones y mantiene las dependencias bajo control.
Es frecuente en interfaces de usuario complejas, salas de chat, controladores de subsistemas y cualquier escenario donde múltiples objetos necesiten colaborar sin generar acoplamientos en red.
Cuando muchos objetos deben comunicarse entre sí, las referencias directas terminan generando grafos densos y difíciles de mantener. Un cambio en un componente puede afectar a varios otros debido a dependencias intrincadas. Además, las reglas de coordinación suelen dispersarse, complicando la extensión y las pruebas.
Mediator propone introducir un objeto central que encapsule la interacción: los colegas notifican al mediador y éste decide qué acciones tomar o a quiénes involucrar. El resultado es una arquitectura más clara donde los componentes permanecen enfocados en su responsabilidad.
La intención del patrón es definir un objeto que encapsule cómo un conjunto de objetos interactúan. Es apropiado cuando:
Mediator favorece el principio de responsabilidad única: los colegas se enfocan en sus funciones y delegan la coordinación en el mediador.
Los elementos esenciales son:
En sistemas modernos es común inyectar el mediador en los colegas, facilitando pruebas y configuración.
El patrón admite distintas variantes:
Seleccionar la variante adecuada evita que el mediador se convierta en un cuello de botella o en un "god object".
Un equipo de soporte opera un tablero donde convergen chats, correos y tickets. El analista debe ver el estado de cada canal sincronizado: si un ticket se marca como urgente, el chat se resalta y se notifica al supervisor; cuando el supervisor lo reasigna, el tablero actualiza la cola del analista.
Mediator permite centralizar esta coordinación: los widgets del tablero (lista de tickets, panel de chat, panel del supervisor) no se conocen entre ellos, sino que informan al mediador quien decide las actualizaciones a propagar.
El siguiente ejemplo en C# implementa un mediador que orquesta la colaboración entre componentes de un tablero de soporte:
using System;
using System.Collections.Generic;
public interface ISupportMediator
{
void RegistrarTicket(Ticket ticket);
void MarcarUrgente(string ticketId);
void Reasignar(string ticketId, string nuevoAnalista);
void RegistrarMensajeChat(string ticketId, string autor, string mensaje);
}
public class SupportMediator : ISupportMediator
{
private readonly PanelTickets _panelTickets;
private readonly PanelChat _panelChat;
private readonly PanelSupervisor _panelSupervisor;
public SupportMediator(PanelTickets panelTickets, PanelChat panelChat, PanelSupervisor panelSupervisor)
{
_panelTickets = panelTickets;
_panelChat = panelChat;
_panelSupervisor = panelSupervisor;
_panelTickets.SetMediator(this);
_panelChat.SetMediator(this);
_panelSupervisor.SetMediator(this);
}
public void RegistrarTicket(Ticket ticket)
{
_panelTickets.Agregar(ticket);
_panelChat.CrearSala(ticket);
_panelSupervisor.NotificarNuevoTicket(ticket);
}
public void MarcarUrgente(string ticketId)
{
var ticket = _panelTickets.MarcarUrgente(ticketId);
if (ticket is not null)
{
_panelChat.DestacarSala(ticketId);
_panelSupervisor.AlertarPrioridad(ticketId);
}
}
public void Reasignar(string ticketId, string nuevoAnalista)
{
if (_panelTickets.ActualizarResponsable(ticketId, nuevoAnalista))
{
_panelChat.RegistrarMensaje(ticketId, "Sistema", $"Ticket reasignado a {nuevoAnalista}");
_panelSupervisor.ConfirmarReasignacion(ticketId, nuevoAnalista);
}
}
public void RegistrarMensajeChat(string ticketId, string autor, string mensaje)
{
_panelChat.RegistrarMensaje(ticketId, autor, mensaje);
_panelTickets.ActualizarUltimaActividad(ticketId, DateTime.UtcNow);
}
}
public abstract class TableroComponent
{
protected ISupportMediator Mediador { get; private set; } = null!;
public void SetMediator(ISupportMediator mediador)
{
Mediador = mediador ?? throw new ArgumentNullException(nameof(mediador));
}
}
public class PanelTickets : TableroComponent
{
private readonly Dictionary<string, Ticket> _tickets = new();
public void Agregar(Ticket ticket)
{
_tickets[ticket.Id] = ticket;
Console.WriteLine($"[PanelTickets] registrado ticket {ticket.Id}");
}
public Ticket? MarcarUrgente(string ticketId)
{
if (_tickets.TryGetValue(ticketId, out var ticket))
{
var actualizado = ticket.MarcarUrgente();
_tickets[ticketId] = actualizado;
Console.WriteLine($"[PanelTickets] ticket {ticketId} marcado urgente");
return actualizado;
}
return null;
}
public bool ActualizarResponsable(string ticketId, string nuevoResponsable)
{
if (_tickets.TryGetValue(ticketId, out var ticket))
{
_tickets[ticketId] = ticket.ActualizarResponsable(nuevoResponsable);
Console.WriteLine($"[PanelTickets] ticket {ticketId} reasignado a {nuevoResponsable}");
return true;
}
return false;
}
public void ActualizarUltimaActividad(string ticketId, DateTime instante)
{
if (_tickets.TryGetValue(ticketId, out var ticket))
{
_tickets[ticketId] = ticket.ActualizarUltimaActividad(instante);
Console.WriteLine($"[PanelTickets] actividad registrada en {ticketId}");
}
}
}
public class PanelChat : TableroComponent
{
private readonly HashSet<string> _salas = new();
public void CrearSala(Ticket ticket)
{
_salas.Add(ticket.Id);
Console.WriteLine($"[PanelChat] sala creada para {ticket.Id}");
}
public void DestacarSala(string ticketId)
{
if (_salas.Contains(ticketId))
{
Console.WriteLine($"[PanelChat] sala {ticketId} destacada por urgencia");
}
}
public void RegistrarMensaje(string ticketId, string autor, string mensaje)
{
if (_salas.Contains(ticketId))
{
Console.WriteLine($"[Chat {ticketId}] {autor}: {mensaje}");
}
}
}
public class PanelSupervisor : TableroComponent
{
public void NotificarNuevoTicket(Ticket ticket)
{
Console.WriteLine($"[Supervisor] Nuevo ticket asignado a {ticket.Responsable}");
}
public void AlertarPrioridad(string ticketId)
{
Console.WriteLine($"[Supervisor] Ticket {ticketId} requiere intervencion urgente");
}
public void ConfirmarReasignacion(string ticketId, string nuevoAnalista)
{
Console.WriteLine($"[Supervisor] Ticket {ticketId} reasignado: {nuevoAnalista}");
}
}
public record Ticket(string Id, string Cliente, string Responsable, DateTime Creada,
bool Urgente, DateTime UltimaActividad)
{
public Ticket MarcarUrgente() => this with { Urgente = true };
public Ticket ActualizarResponsable(string nuevoResponsable) => this with { Responsable = nuevoResponsable };
public Ticket ActualizarUltimaActividad(DateTime instante) => this with { UltimaActividad = instante };
}
public static class AplicacionMediator
{
public static void Main()
{
var panelTickets = new PanelTickets();
var panelChat = new PanelChat();
var panelSupervisor = new PanelSupervisor();
var mediador = new SupportMediator(panelTickets, panelChat, panelSupervisor);
var ticket = new Ticket("TCK-001", "Acme Corp", "Sofia", DateTime.UtcNow, false, DateTime.UtcNow);
mediador.RegistrarTicket(ticket);
mediador.RegistrarMensajeChat("TCK-001", "Cliente", "Tengo un problema con la factura.");
mediador.MarcarUrgente("TCK-001");
mediador.Reasignar("TCK-001", "Marcos");
}
}
El mediador recibe las operaciones clave (registrar, marcar urgente, reasignar, mensajes) y coordina acciones entre los paneles. Los colegas se invocan a través del mediador, manteniéndose libres de referencias mutuas. Esto permite reemplazar paneles o agregar nuevos canales (por ejemplo, notificaciones push) modificando solo al mediador.
El estado de los tickets se encapsula en un record de C#, lo que favorece la inmutabilidad; los paneles pueden actualizarlo devolviendo nuevas instancias si se requiere persistencia fuera del ejemplo.
En .NET es habitual recurrir al patrón para coordinar vistas y componentes en WPF o Blazor, evitando dependencias directas entre controles. Bibliotecas como MediatR proporcionan un mediador request/response que desacopla controladores de la lógica de dominio, mientras que ASP.NET Core permite integrar estos mediadores para encapsular comandos y notificaciones.
Mediator se puede combinar con distintas estrategias de orquestación:
Estas variantes permiten adaptar el patrón a arquitecturas distribuidas o a soluciones que requieren alto nivel de configurabilidad.
Un riesgo común es convertir al mediador en un “god object” que concentra demasiadas responsabilidades. También es problemático cuando los colegas conservan referencias cruzadas, lo que invalida el desacoplamiento buscado.
Otro anti-patrón es implementar mediadores que solo reenvían llamadas sin agregar coordinación real; en ese caso, es preferible usar Observer o simples invocaciones directas.
Mediator se complementa con Observer cuando los colegas necesitan suscribirse a eventos generados por el mediador. Puede trabajar con Command encapsulando solicitudes que el mediador reenvía. Además, comparte similitudes con Facade al ofrecer un punto de acceso unificado, aunque Facade se centra en simplificar APIs y Mediator en coordinar interacciones entre colegas.