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

12 KiB
Raw Blame History

← Design patterns index

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.
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.
  • 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