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

  • 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. Component should 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 use weak_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