12 KiB
Mediator
Category: Behavioral | Also known as: Intermediary, Controller |
Source: patterns/mediator/AirTrafficControl/
Problem
An airport tower needs to arbitrate five runways among any number of flights
and ground vehicles. Wire that directly, and each Flight and GroundVehicle
needs its own reference to the runway array and to every other aircraft
currently occupying a runway. Every colleague then duplicates the same
allocation logic. A new colleague type, such as a snowplow, needs the same
wiring again.
Solution
Give every colleague exactly one reference: to a mediator. Colleagues stop
tracking runway state themselves — they send a request (RequestLanding,
RequestTakeoff, RequestRunwayCrossing, …) to the mediator and wait to be
told accepted: true/false. The mediator alone owns the five Runway slots
and decides who gets a runway. Colleagues never learn about each other at all.
Participants (GoF names → this repo)
- Mediator →
IMediator— declaresNotify(sender, request). - ConcreteMediator →
AirTrafficControlTower— owns the five runways and is the only class that knows the allocation rules. - Colleague →
IColleague— declaresReceiveMessage(instruction, accepted). - ConcreteColleague →
Flight,GroundVehicle— send requests, receive accept/reject callbacks; neither knows the other exists.
classDiagram
class IMediator {
<<interface>>
+Notify(sender, request)
}
class IColleague {
<<interface>>
+ReceiveMessage(instruction, accepted)
}
class AirTrafficControlTower {
-Runway[5] _runways
+Notify(sender, request)
}
class Flight {
+RequestLanding()
+RequestTakeoff()
}
class GroundVehicle {
+RequestRunwayCrossing()
}
IMediator <|.. AirTrafficControlTower
IColleague <|.. Flight
IColleague <|.. GroundVehicle
Flight --> IMediator : Notify(this, request)
GroundVehicle --> IMediator : Notify(this, request)
AirTrafficControlTower --> IColleague : ReceiveMessage(instruction, accepted)
Note the arrow direction: Flight/GroundVehicle only ever point at
IMediator; AirTrafficControlTower only ever points back at IColleague.
Neither colleague type points at the other.
Code
// Program.cs — Mediator + Colleague interfaces
public interface IColleague
{
void ReceiveMessage(string instruction, bool accepted);
}
public interface IMediator
{
void Notify(IColleague sender, AirTrafficRequest request);
}
// Flight — ConcreteColleague: only ever talks to _mediator
public class Flight : IColleague
{
private readonly IMediator _mediator;
public string FlightNumber { get; }
public Flight(string flightNumber, IMediator mediator)
{
FlightNumber = flightNumber;
_mediator = mediator;
}
public void RequestLanding() =>
_mediator.Notify(this, AirTrafficRequest.LandingRequest);
public void LandingCompleted() =>
_mediator.Notify(this, AirTrafficRequest.LandingRequestDone);
public void ReceiveMessage(string instruction, bool accepted) =>
Console.WriteLine(accepted
? $"Flight {FlightNumber} received instruction: {instruction}"
: $"Flight {FlightNumber} received rejection for instruction: {instruction}");
// RequestTakeoff() / TakeoffCompleted() follow the same shape
}
// GroundVehicle mirrors Flight exactly, but for RequestRunwayCrossing() /
// RunwayCrossingCompleted() — same interface, same "only knows the mediator" shape
// AirTrafficControlTower — ConcreteMediator: the only class that knows
// about runways and occupancy
public class AirTrafficControlTower : IMediator
{
private class Runway
{
public string Name { get; init; } = "";
public IColleague? OccupiedBy { get; set; }
public Runway(string name) => Name = name;
}
private readonly Runway[] _runways = new Runway[5];
public AirTrafficControlTower()
{
for (int i = 0; i < _runways.Length; i++)
_runways[i] = new Runway($"Runway R{i + 1}");
}
public void Notify(IColleague sender, AirTrafficRequest request)
{
switch (request)
{
case AirTrafficRequest.LandingRequest: HandleLandingRequest(sender); break;
case AirTrafficRequest.TakeoffRequest: HandleTakeoffRequest(sender); break;
case AirTrafficRequest.RunwayCrossingRequest: HandleRunwayCrossingRequest(sender); break;
case AirTrafficRequest.LandingRequestDone: HandleLandingRequestDone(sender); break;
case AirTrafficRequest.TakeoffRequestDone: HandleTakeoffRequestDone(sender); break;
case AirTrafficRequest.RunwayCrossingRequestDone: HandleRunwayCrossingRequestDone(sender); break;
}
}
void HandleLandingRequest(IColleague sender)
{
for (int i = 0; i < _runways.Length; i++)
{
if (_runways[i].OccupiedBy is null)
{
_runways[i].OccupiedBy = sender;
sender.ReceiveMessage($"Landing approved on {_runways[i].Name}.", accepted: true);
return;
}
}
sender.ReceiveMessage("No available runways for landing.", accepted: false);
}
// HandleTakeoffRequest / HandleRunwayCrossingRequest mirror
// HandleLandingRequest; the three Handle*Done methods clear whichever
// runway `sender` occupies
}
// Program.cs — client: flat construction, then requests through the tower only
var tower = new AirTrafficControlTower();
var flight1 = new Flight("AC101", tower);
var vehicle1 = new GroundVehicle("V1", tower);
flight1.RequestLanding(); // approved: Runway R1 free
// ... 4 more requests fill R2–R5 ...
vehicle2.RequestRunwayCrossing(); // rejected: no runways left
flight1.LandingCompleted(); // frees R1
vehicle2.RequestRunwayCrossing(); // now approved, on the freed R1
Output (verified)
--- Occupying all 5 runways ---
Flight AC101 requesting landing.
Flight AC101 received instruction: Landing approved on Runway R1.
Flight WJ202 requesting takeoff.
Flight WJ202 received instruction: Takeoff approved on Runway R2.
Flight DL303 requesting landing.
Flight DL303 received instruction: Landing approved on Runway R3.
Flight UA404 requesting takeoff.
Flight UA404 received instruction: Takeoff approved on Runway R4.
Ground vehicle V1 requesting runway crossing.
Ground vehicle V1 received instruction: Crossing approved on Runway R5.
--- No runways left: this request should be rejected ---
Ground vehicle V2 requesting runway crossing.
Ground vehicle V2 received rejection for instruction: No available runways for crossing.
--- Freeing up a runway: the previously rejected request can now succeed ---
Flight AC101 has completed landing.
Flight AC101 received instruction: You have cleared Runway R1 after landing.
Ground vehicle V2 requesting runway crossing.
Ground vehicle V2 received instruction: Crossing approved on Runway R1.
--- Clearing the remaining runways ---
Flight WJ202 has completed takeoff.
Flight WJ202 received instruction: You have cleared Runway R2 after takeoff.
Flight DL303 has completed landing.
Flight DL303 received instruction: You have cleared Runway R3 after landing.
Flight UA404 has completed takeoff.
Flight UA404 received instruction: You have cleared Runway R4 after takeoff.
Ground vehicle V1 has completed runway crossing.
Ground vehicle V1 received instruction: You have cleared Runway R5 after crossing.
Ground vehicle V2 has completed runway crossing.
Ground vehicle V2 received instruction: You have cleared Runway R1 after crossing.
GoF pseudocode (dialog coordinating its form controls)
interface Mediator is
method notify(sender: Component, event: string)
class AuthenticationDialog implements Mediator is
field loginOrRegisterChkBx: Checkbox
field okBtn: Button
method notify(sender, event) is
if (sender == loginOrRegisterChkBx and event == "check")
// show/hide login vs. registration fields, retitle the dialog
if (sender == okBtn and event == "click")
// validate credentials or create the account
class Component is
field dialog: Mediator
constructor Component(dialog) is this.dialog = dialog
method click() is dialog.notify(this, "click")
class Checkbox extends Component is
method check() is dialog.notify(this, "check")
This repo's version tracks the canonical shape closely:
Notify(sender, request) takes the same two arguments as GoF's
notify(sender, event) — only an AirTrafficRequest enum in place of a bare
string, and an IMediator interface, so colleagues aren't wired to one concrete
mediator class. Where the GoF sketch dispatches on sender identity inside a
single notify, this repo dispatches on the request type and lets five
Handle* methods each scan _runways — same idea, more moving parts because
there are more request kinds than the two-widget dialog example needs.
Applicability
- A set of objects communicate in well-defined but complex ways, and the resulting interdependencies are unstructured and hard to follow.
- Reusing an object is hard because it refers to and talks to many others.
- Behavior distributed across several classes should be customizable without lots of subclassing — write a new mediator instead.
Pros / Cons
| Pros | Cons |
|---|---|
| Single Responsibility — collaboration logic sits in one place | The mediator can become a God Object / monolith |
| Open/Closed — new mediators without touching components | It centralizes control: complexity moves rather than vanishes |
| Components decouple and become individually reusable | Five near-identical Handle* methods here already show the mediator absorbing complexity the colleagues no longer carry |
| Replaces many-to-many interactions with one-to-many |
Seen in the wild (.NET)
- WinForms/WPF code-behind acting as dialog director.
- The MediatR library.
- SignalR hubs routing between clients.
- A real air-traffic-control tower — this repo's own example, and the book's analogy: pilots never talk to each other, only to the tower.
Related patterns
- Chain of Responsibility / Command / Mediator / Observer are the four ways to connect senders and receivers: CoR passes along a chain until handled; Command makes a one-way sender→receiver link; Mediator removes direct links, forcing a hub; Observer lets receivers subscribe dynamically.
- Facade vs. Mediator — directionality (see Facade).
- Mediator vs. Observer — genuinely blurry. Distinguishing goal: Mediator eliminates mutual dependencies among a known set of components by routing every interaction through a fixed hub; Observer establishes dynamic one-way subscriptions where any number of unknown subscribers can attach later.
← Prev: Facade · All patterns · Next → Memento