Mediator Pattern
- Description: Centralize complex many-to-many interactions among components into a single mediator object, so components only talk to the mediator, not to each other.
- My Notion Note ID: K2C-2-16
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Why It Exists
- 2. Structure
- 3. C++ Example
- 4. When to Use / When Not To
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Why It Exists
- N components talking directly = O(N²) edges. Each one knows the others.
- Add a component → wire it to every existing one. Remove one → audit every neighbor.
- Mediator collapses the graph: every component knows only the Mediator. N edges.
- Behavior moves out of components into the Mediator → reusable components, complex Mediator.
Typical uses:
- GUI dialogs — checkbox toggles enable/disable other widgets, label updates, list filters, button states.
- ATC / chat rooms — planes talk to tower, not each other; users send to room, not peers.
- Event bus / signal-slot wiring — Qt's
QObject::connect, boost::signals2. - Air-traffic in distributed systems — message broker (Kafka, RabbitMQ) is Mediator at infra scale.
2. Structure
Roles:
- Mediator — interface declaring
notify(sender, event)(or per-event methods). - ConcreteMediator — knows all Colleagues; encodes coordination logic.
- Colleague — holds a
Mediator*; sends events via mediator, receives commands from it.
┌─ Colleague A ─┐
│ ├─→ Mediator ←─┤ Colleague C │
└─ Colleague B ─┘ └─────────────┘
Compare classic O(N²) wiring vs star topology — that's the whole point.
3. C++ Example
3.1 Dialog mediator (classic GoF)
#include <string>
#include <iostream>
class Mediator;
class Component {
protected:
Mediator* m_ = nullptr;
public:
void set_mediator(Mediator* m) { m_ = m; }
virtual ~Component() = default;
};
class Mediator {
public:
virtual ~Mediator() = default;
virtual void notify(Component* sender, const std::string& event) = 0;
};
class Button : public Component {
public:
std::string label;
void click();
};
class Checkbox : public Component {
public:
bool checked = false;
void toggle();
};
class TextField : public Component {
public:
std::string text;
void set(const std::string& s) { text = s; std::cout << "Field=" << s << "\n"; }
};
class LoginDialog : public Mediator {
Button* submit_;
Checkbox* remember_;
TextField* status_;
public:
LoginDialog(Button* s, Checkbox* r, TextField* t)
: submit_(s), remember_(r), status_(t)
{
s->set_mediator(this);
r->set_mediator(this);
t->set_mediator(this);
}
void notify(Component* sender, const std::string& event) override {
if (sender == remember_ && event == "toggled") {
status_->set(remember_->checked ? "Will remember login" : "");
} else if (sender == submit_ && event == "clicked") {
status_->set("Submitting...");
}
}
};
void Button::click() { if (m_) m_->notify(this, "clicked"); }
void Checkbox::toggle() { checked = !checked; if (m_) m_->notify(this, "toggled"); }
int main() {
Button submit; submit.label = "OK";
Checkbox remember;
TextField status;
LoginDialog dlg(&submit, &remember, &status);
remember.toggle(); // Field=Will remember login
submit.click(); // Field=Submitting...
}
Buttons and Checkboxes know nothing of each other. Add a new field → only LoginDialog::notify changes.
3.2 Event-driven Mediator (signal/slot style)
When events are typed and many, an if-cascade gets ugly. Use a signal hub.
#include <functional>
#include <unordered_map>
#include <vector>
#include <string>
class EventBus {
std::unordered_map<std::string, std::vector<std::function<void(const std::string&)>>> subs_;
public:
void on(const std::string& topic, std::function<void(const std::string&)> cb) {
subs_[topic].push_back(std::move(cb));
}
void publish(const std::string& topic, const std::string& payload) {
auto it = subs_.find(topic);
if (it == subs_.end()) return;
for (auto& cb : it->second) cb(payload);
}
};
// Components publish/subscribe via EventBus — they never reference each other.
With boost::signals2, this is even leaner — see Observer note for thread-safe variant.
4. When to Use / When Not To
Use when:
- A set of components has dense, complex interaction logic.
- Coordination logic changes often, independent of components.
- You want to reuse components in different contexts (different Mediator wires them differently).
- Component count keeps growing — pairwise wiring is becoming unmanageable.
Don't use when:
- Components only interact in a few simple, stable ways → direct calls or Observer.
- Mediator would have only 2 components — overhead without benefit.
- Coordination logic naturally lives inside one component (the others are clearly subordinate).
5. Variants and Pitfalls
Variants:
- Explicit Mediator interface + per-event methods (
on_button_clicked,on_text_changed) — more type-safe than string events, less flexible. - Event bus / publish-subscribe — Mediator generalized; components broadcast, others subscribe.
- Signal/slot framework (Qt
QObject, boost::signals2) — Mediator wired at connect-time, fires automatically. - Message broker (Kafka, RabbitMQ, NATS) — distributed Mediator.
Pitfalls:
- God-class Mediator. All coordination logic centralizes — Mediator grows monstrous. Split into multiple Mediators per concern (toolbar Mediator, sidebar Mediator).
- Components leak knowledge of Mediator type.
Componentshould know only the abstract Mediator interface; ConcreteMediator-specific calls reintroduce coupling. - Hidden control flow. Event handlers fire in non-obvious order. Debugging gets painful — single-step misses the indirection. Logging at the Mediator helps.
- Component lifetime vs Mediator. Components store
Mediator*; Mediator destroyed first → dangling. Either Mediator owns components, or useweak_ptr. - Mediator inversion. Components calling Mediator from inside Mediator's own notify path → re-entrancy bugs. Defer with a queue if needed.
6. Related Patterns
- Mediator vs Facade — both add an intermediary, but opposite directions and awareness:
- Facade — unidirectional: client → facade → subsystem. Subsystem is unaware of facade. Simplifies access to a complex API.
- Mediator — bidirectional: colleagues ↔ mediator. Colleagues know the mediator and depend on it. Coordinates collaboration.
- Mediator vs Observer — see Observer note for the canonical contrast. TL;DR:
- Observer — distributed notification, broadcast, peers don't know each other.
- Mediator — centralized coordination, hub knows all peers, peers know hub.
- They combine: Mediator often implemented using Observer (Mediator = subject + custom dispatch logic).
- Mediator vs Command — Mediator coordinates components; Command encapsulates a request. Mediator may dispatch Commands.
- Mediator vs Singleton — Mediator is often Singleton-scoped (one per dialog/app). But Singleton is structural; Mediator is behavioral. Don't conflate.
- Mediator vs Service Locator — Service Locator finds services; Mediator coordinates interactions. Both centralize, different concerns.
7. References
- Design Patterns (GoF), ch. 5.
- Refactoring.Guru — Mediator
- Boost.Signals2
- Qt —
QObject::connectis Mediator at framework scale. - Kafka, RabbitMQ — distributed Mediators (message brokers).