Aller au contenu principal

Pattern Mediator

Définissez un coordinateur central qui gère la communication entre objets pour qu'ils ne se référencent pas directement.


Le Problème

Vous construisez un tableau de bord de vol pour le contrôle aérien. Trois panneaux : liste des vols, météo et état des pistes. Quand la météo passe en "tempête", certains vols doivent être retardés et la piste doit passer en capacité réduite. Quand vous cliquez sur un vol, le panneau météo doit zoomer sur sa destination et le panneau piste doit afficher la porte d'embarquement.

L'approche naïve :

function updateWeather(condition: string) {
weatherPanel.display(condition);
flightList.delayFlightsFor(condition); // knows about flightList
runwayStatus.adjustCapacity(condition); // knows about runwayStatus
}

function selectFlight(flightId: string) {
flightList.highlight(flightId);
weatherPanel.zoomTo(flight.destination); // knows about weatherPanel
runwayStatus.showGate(flight.gate); // knows about runwayStatus
}

Chaque panneau connaît tous les autres. Ajouter un quatrième panneau (infos passagers, état du carburant) implique de modifier tous les panneaux existants.


La Solution

Les panneaux ne connaissent que le mediator. Ils émettent des événements, le mediator les route :

import { createMediator } from "@pithos/core/eidos/mediator/mediator";

type DashboardEvents = {
weatherChanged: { condition: string; severity: number };
flightSelected: { flightId: string; destination: string; gate: string };
runwayUpdated: { capacity: "full" | "reduced" | "closed" };
};

const dashboard = createMediator<DashboardEvents>();

// Weather panel reacts to flight selection
dashboard.on("flightSelected", ({ destination }) => {
weatherPanel.zoomTo(destination);
});

// Flight list reacts to weather changes
dashboard.on("weatherChanged", ({ condition, severity }) => {
if (severity > 7) flightList.delayAll();
dashboard.emit("runwayUpdated", { capacity: "reduced" });
});

// Runway reacts to weather
dashboard.on("runwayUpdated", ({ capacity }) => {
runwayStatus.setCapacity(capacity);
});

// Panels emit events without knowing who listens
weatherPanel.onChange = (condition, severity) =>
dashboard.emit("weatherChanged", { condition, severity });

Les panneaux sont découplés. Ajoutez un panneau carburant sans toucher à la météo, aux vols ou aux pistes.


Démo

Tableau de bord de vol DGAC où les panneaux météo, vols et pistes communiquent exclusivement via un mediator, avec chaque événement visible dans un log en temps réel.

Code
/**
* DGAC Flight Dashboard: Mediator demo.
*
* Three panels (flights, weather, runway) communicate exclusively
* through a typed mediator. No panel knows about the others.
*/

import { createMediator } from "@pithos/core/eidos/mediator/mediator";
import { INITIAL_FLIGHTS, WEATHER_SEVERITY, CAPACITY_FROM_SEVERITY, applyWeatherToFlights } from "./data";
import type { WeatherCondition, RunwayCapacity, DashboardEvents, DashboardState } from "./types";

// Re-export for consumers
export type { WeatherCondition, RunwayCapacity, Flight, LogEntry, DashboardState } from "./types";
export { WEATHER_OPTIONS } from "./types";

export function createDashboard() {
const mediator = createMediator<DashboardEvents>();
let flights = INITIAL_FLIGHTS.map((f) => ({ ...f }));
let weather: WeatherCondition = "clear";
let severity = 0;
let runway: RunwayCapacity = "full";
let selectedFlightId: string | null = null;
let logId = 0;
const logs: { id: number; timestamp: string; event: string; detail: string }[] = [];

let snapshot = buildSnapshot();
const listeners = new Set<() => void>();

function now() {
return new Date().toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}

function addLog(event: string, detail: string) {
logs.push({ id: ++logId, timestamp: now(), event, detail });
if (logs.length > 50) logs.shift();
}

function buildSnapshot(): DashboardState {
return {
flights: flights.map((f) => ({ ...f })),
weather,
severity,
runway,
selectedFlightId,
logs: [...logs],
};
}

function commit() {
snapshot = buildSnapshot();
for (const fn of listeners) fn();
}

// ── Wire up mediator handlers ──────────────────────────────────

mediator.on("weatherChanged", ({ condition, severity: sev }) => {
weather = condition;
severity = sev;
const changes = applyWeatherToFlights(flights, condition);
for (const c of changes) addLog("flightStatusChanged", c);
const cap = CAPACITY_FROM_SEVERITY(sev);
mediator.emit("runwayUpdated", { capacity: cap });
addLog("weatherChanged", `${condition} (severity ${sev})`);
});

mediator.on("runwayUpdated", ({ capacity }) => {
runway = capacity;
addLog("runwayUpdated", `capacity → ${capacity}`);
});

mediator.on("flightSelected", ({ flightId, destination, gate }) => {
selectedFlightId = flightId;
addLog("flightSelected", `${flightId}${destination} (gate ${gate})`);
});

mediator.on("flightDeselected", () => {
selectedFlightId = null;
addLog("flightDeselected", "selection cleared");
});

// ── Public API ─────────────────────────────────────────────────

return {
subscribe(fn: () => void) {
listeners.add(fn);
return () => { listeners.delete(fn); };
},

getState(): DashboardState {
return snapshot;
},

setWeather(condition: WeatherCondition) {
mediator.emit("weatherChanged", { condition, severity: WEATHER_SEVERITY[condition] });
commit();
},

selectFlight(flightId: string) {
const f = flights.find((fl) => fl.id === flightId);
if (f) {
mediator.emit("flightSelected", { flightId: f.id, destination: f.destination, gate: f.gate });
commit();
}
},

deselectFlight() {
mediator.emit("flightDeselected", {});
commit();
},

reset() {
flights = INITIAL_FLIGHTS.map((f) => ({ ...f }));
weather = "clear";
severity = 0;
runway = "full";
selectedFlightId = null;
logs.length = 0;
logId = 0;
commit();
},
};
}
Result

Analogie

Une tour de contrôle aérien. Les avions ne communiquent pas directement entre eux : ce serait le chaos. Ils parlent tous à la tour, qui coordonne les décollages, atterrissages et trajectoires de vol. La tour est le mediator.


Quand l'Utiliser

  • Plusieurs composants doivent communiquer sans se connaître
  • Vous voulez centraliser la logique d'interaction complexe
  • Ajouter de nouveaux composants ne doit pas nécessiter de modifier les existants
  • Vous avez besoin d'un log de tous les messages inter-composants

Quand NE PAS l'Utiliser

Si deux composants ont une relation parent-enfant simple, des props/callbacks directs sont plus clairs. Ne routez pas tout via un mediator quand un appel de fonction suffit.


API

  • createMediator — Créer un hub d'événements typé pour une communication découplée