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.
- dashboard.ts
- Usage
/**
* 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();
},
};
}
import { useState } from "react";
import { RotateCcw, Terminal, X } from "lucide-react";
import { useDashboard } from "@/hooks/useDashboard";
import { BRAND_COLOR } from "./constants";
import { WeatherPanel } from "./WeatherPanel";
import { FlightListPanel } from "./FlightListPanel";
import { RunwayPanel } from "./RunwayPanel";
import { MediatorLog } from "./MediatorLog";
export function FlightDashboard() {
const {
state, selectedFlight, logScrollRef,
handleWeather, handleSelectFlight, handleReset,
} = useDashboard();
const [logOpen, setLogOpen] = useState(false);
return (
<div className="h-screen flex flex-col bg-slate-100 overflow-hidden">
{/* Header */}
<div className="shrink-0 px-4 py-2.5" style={{ background: BRAND_COLOR }}>
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="dgac-logo.svg" alt="DGAC" className="h-8 w-8 object-contain brightness-0 invert" />
<div>
<h1 className="text-sm font-bold text-white leading-none">DGAC Flight Control</h1>
<p className="text-[10px] text-blue-200/70 mt-0.5">
3 panels, 1 mediator : <code className="text-blue-100/80">createMediator()</code>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleReset}
className="p-2 text-blue-200/60 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={() => setLogOpen(!logOpen)}
className="md:hidden p-2 bg-white/10 rounded-lg relative"
>
{logOpen ? <X className="w-4 h-4 text-white/70" /> : <Terminal className="w-4 h-4 text-white" />}
{!logOpen && !!state.logs.length && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-[8px] font-bold text-white flex items-center justify-center">
{state.logs.length}
</span>
)}
</button>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 min-h-0">
{logOpen && (
<div className="md:hidden h-full p-3">
<MediatorLog logs={state.logs} scrollRef={logScrollRef} />
</div>
)}
<div className={`h-full max-w-6xl mx-auto px-1 sm:px-4 py-1 ${logOpen ? "hidden md:grid" : "flex flex-col md:grid"} md:grid-cols-5 md:gap-4`}>
<div className="md:col-span-3 overflow-y-auto min-h-0 space-y-3 pb-2">
<WeatherPanel
current={state.weather}
severity={state.severity}
destination={selectedFlight?.destination}
onChangeWeather={handleWeather}
/>
<div className="space-y-3">
<FlightListPanel
flights={state.flights}
selectedId={state.selectedFlightId}
onSelect={handleSelectFlight}
/>
<RunwayPanel
capacity={state.runway}
gate={selectedFlight?.gate}
flightId={selectedFlight?.id}
/>
</div>
</div>
<div ref={logScrollRef} className="hidden md:flex md:col-span-2 flex-col overflow-y-auto min-h-0">
<MediatorLog logs={state.logs} />
</div>
</div>
</div>
<div className="shrink-0 py-1.5 text-center text-[10px] text-slate-400">
Panels communicate through <code className="text-slate-500 font-medium">createMediator()</code> : no direct references
</div>
</div>
);
}
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