Home Design Patterns Mediator
Mediator
Behavioral Gang of Four“Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.”
Gamma, Helm, Johnson & Vlissides, p. 273Purpose
A Mediator is a hub. A set of objects that would otherwise call each other directly all talk to it instead, and it decides who needs to do what. Each colleague ends up knowing one thing, the mediator, rather than knowing every other participant.
The gain is arithmetic before it is anything else. Five components that all talk to each other have twenty possible directed relationships, and every one of them is a reference someone has to maintain. Five components around a mediator have five. The cost is that the interaction knowledge has to go somewhere, and where it goes is into the mediator, which is now the most important and most complicated class in the group.
Design
Colleagues hold a reference to the Mediator interface and notify it when something happens to them. They do not name the recipient, because they do not know there is one. The ConcreteMediator holds references to the concrete colleagues and contains all of the logic that used to be spread across their call sites.
How a colleague notifies the mediator is the design decision that decides
whether this ages well. A single notify(sender, event) method is
flexible and turns the mediator into a large conditional. A method per
interaction is more typing and far easier to read three years later. The middle
option, and the one I would defend, is to make the mediator's interface a set of
named domain operations, so it reads as a description of the process rather than
as a dispatcher.
Example
An Autobot Robotics work cell is a conveyor, an Arm802, a vision camera, a safety scanner and a light stack, and they interlock constantly. A part arrives, so the belt stops and the camera triggers. The camera passes the board, so the arm picks and places. The camera rejects it, so the belt diverts instead. The scanner trips, so everything stops and the light stack goes red.
All of that was originally written where it happened. The conveyor object held a reference to the camera and the arm. The camera held the arm and the conveyor. The scanner held everything. Adding a second camera for the underside of the board meant editing four classes that had nothing to do with cameras, and the team could not answer the question "what happens when a board is rejected?" without reading five files, because the answer was distributed across all of them.
A CellCoordinator fixed the immediate problem. Devices now report
what happened to them and nothing else, the coordinator decides what follows, and
the sequence for a rejected board is fifteen readable lines in one place. I will
be honest about the sequel: within two years the coordinator was the largest
class in the cell software and every change to any device touched it. The team
eventually split it into one coordinator per cell layout, which is the correct
answer and one they should have started with.
interface CellMediator
{
void notify(Device sender, string what);
}
abstract class Device
{
protected CellMediator cell;
public Device(CellMediator m) { cell = m; }
}
class Conveyor : Device
{
public void partArrived() { cell.notify(this, "partArrived"); }
public void stop() { /* some code */ }
public void divert() { /* some code */ }
}
class Camera : Device { } // raises "pass" and "reject"
class Arm : Device { } // offers pickAndPlace()
class CellCoordinator : CellMediator
{
private Conveyor belt;
private Camera camera;
private Arm arm;
public void notify(Device sender, string what) {
if (sender == belt && what == "partArrived") {
belt.stop();
camera.trigger();
}
else if (sender == camera && what == "pass") {
arm.pickAndPlace();
}
else if (sender == camera && what == "reject") {
belt.divert();
}
}
}
Conveyor never mentions
Camera. It is also fair to notice that
notify() is already a growing conditional, which is this pattern's
characteristic failure and the reason I prefer named operations to a generic
notify.Seen in the Wild
The Gang of Four's own example, a dialog box class that wires up its own widgets so that changing one control enables or disables another, is still the most common real instance: it is what most form code and what the presenter in Model-View-Presenter actually is. MediatR is a widely used .NET library named directly after the pattern, although what it provides is closer to an in-process request dispatcher than to the GoF structure. At a larger scale, a message broker such as RabbitMQ is the same idea between processes rather than between objects, with the routing rules playing the part of the mediator's logic.
Comparisons
- Observer is usually how colleagues tell the mediator something. The two are so often combined that people confuse them: Observer is the notification mechanism, Mediator is the thing that decides what the notification means.
- Facade also centralizes, but the traffic is one way and the subsystem does not know the facade exists. Colleagues know their mediator and talk back to it.
- Chain of Responsibility decouples sender from receiver too, using a line rather than a hub. The routing decision is distributed along the chain instead of centralized.
- Command often supplies what colleagues send. A mediator that receives command objects instead of string event names is considerably easier to live with.
- Singleton is what a mediator turns into when nobody is watching, and that combination is where this pattern goes from useful to harmful. See the criticisms.
Why It Exists
My take
Mediator exists because interaction logic has no natural home. Every other responsibility in a system belongs to some object: the conveyor knows how to move a belt, the camera knows how to expose a frame. But the rule that a rejected board diverts rather than gets picked belongs to none of them. It is about the relationship, not the participants, and if you refuse to give it a home it gets smeared across all of them in fragments.
That smearing is the actual damage, more than the reference count. When interaction logic lives in twelve call sites, nobody can read the process. You cannot answer "what happens when a board is rejected" by reading a file, because there is no file. And interaction logic is precisely the part that changes most often, because it is where the customer's process lives. A Mediator is the decision to make the most volatile knowledge in the system also the most visible.
My position is that this pattern is only a win at small scope. A mediator for one dialog, one work cell, one screen: good, because there is a bounded set of collaborators and a bounded process. A mediator for an application: not a pattern, just a god object with a diagram. The tell is the constructor. When the mediator's constructor takes nine colleagues you are past the point where centralizing helped, and you should be looking for two mediators with a seam between them.
Criticisms
The mediator becomes a god object. This is not an occasional risk, it is the normal trajectory, and it happens because the pattern actively encourages it: every new interaction is another branch in one class, and adding it there is always the smallest possible change. The class grows monotonically, nobody ever has a reason to split it, and eventually every change to any colleague requires editing the one file everybody else is also editing.
It also does not remove coupling so much as make it invisible. Colleagues no
longer name each other, but they now depend on a protocol: the event names they
raise, the order the mediator processes things in, and the assumption that
somebody will react. None of that is checked by a compiler. With a generic
notify(sender, string) you cannot even find the handler for an event
by searching for references, which is a real loss compared to the direct calls
you replaced.
Then there is the Singleton problem. A mediator is convenient, so somebody makes it globally reachable, and now every class in the system can join the conversation. At that point it is an application-wide event bus, and event buses are notorious for producing systems where nobody can trace a cause to an effect. If your mediator is static, it has already stopped being this pattern.