Add code examples, and documentation

This commit is contained in:
Aaron Po
2026-08-12 00:03:32 -04:00
commit 77c16e69f6
79 changed files with 4458 additions and 0 deletions

284
docs/mediator.md Normal file
View File

@@ -0,0 +1,284 @@
[← 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)