Files
Design-Patterns/docs/mediator.md
2026-08-13 22:47:17 -04:00

285 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[← 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)