285 lines
12 KiB
Markdown
285 lines
12 KiB
Markdown
[← Design patterns index](README.md)
|
||
|
||
# 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` — declares `Notify(sender, request)`.
|
||
- **ConcreteMediator** → `AirTrafficControlTower` — owns the five runways and
|
||
is the only class that knows the allocation rules.
|
||
- **Colleague** → `IColleague` — declares
|
||
`ReceiveMessage(instruction, accepted)`.
|
||
- **ConcreteColleague** → `Flight`, `GroundVehicle` — send requests, receive
|
||
accept/reject callbacks; neither knows the other exists.
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```csharp
|
||
// 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
|
||
}
|
||
```
|
||
|
||
```csharp
|
||
// 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](facade.md) · [All patterns](README.md) · **Next →**
|
||
[Memento](memento.md)
|