Skip to content

Mediator Pattern

Overview

The Mediator pattern is a behavioral design pattern that defines 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.

Intent

  • Define an object that encapsulates how a set of objects interact
  • Promote loose coupling by keeping objects from referring to each other explicitly
  • Let you vary object interactions independently
  • Centralize complex communications and control between related objects

Problem

Imagine you're building a complex dialog form with multiple UI components: text fields, buttons, checkboxes, dropdown lists, etc. These components need to interact with each other in various ways:

  • A checkbox might enable/disable certain text fields
  • A button's enabled state depends on whether required fields are filled
  • Changing a dropdown selection might update other fields
  • Clearing one field might trigger clearing of related fields

If you implement this directly, each component would need references to many other components, leading to:

  • Tight coupling: Each component knows about many others
  • Difficult to maintain: Changing one component affects many others
  • Hard to reuse: Components can't be used independently
  • Complex dependencies: Web of interconnections between objects

The system becomes increasingly difficult to modify and understand as complexity grows.

Solution

The Mediator pattern suggests that you should cease all direct communication between the components which you want to make independent of each other. Instead, these components must collaborate indirectly, by calling a special mediator object that redirects the calls to appropriate components. As a result, the components depend only on a single mediator class instead of being coupled to dozens of their colleagues.

Structure

Mediator Pattern Diagram 0

Components

  1. Mediator: Defines an interface for communicating with Colleague objects.

  2. ConcreteMediator: Implements the Mediator interface and coordinates communication between Colleague objects. Knows and maintains its colleagues.

  3. Colleague: Each Colleague class knows its Mediator object. Each colleague communicates with its mediator whenever it would have otherwise communicated with another colleague.

Implementation Details

Mediator Interface

1
2
3
4
5
6
class Mediator {
public:
    virtual ~Mediator() = default;
    virtual void notify(const std::string& message, 
                       Colleague* colleague) = 0;
};

Colleague Base Class

class Colleague {
protected:
    Mediator* mediator_;

public:
    explicit Colleague(Mediator* mediator = nullptr) 
        : mediator_(mediator) {}

    virtual ~Colleague() = default;

    void setMediator(Mediator* mediator) {
        mediator_ = mediator;
    }
};

Concrete Mediator

class ConcreteMediator : public Mediator {
private:
    std::vector<Colleague*> colleagues_;

public:
    void registerColleague(Colleague* colleague) {
        colleagues_.push_back(colleague);
        colleague->setMediator(this);
    }

    void notify(const std::string& message, 
                Colleague* sender) override {
        // Route message to appropriate colleagues
        for (auto* colleague : colleagues_) {
            if (colleague != sender) {
                // Notify colleague
            }
        }
    }
};

Real-World Example 1: Chat Room

Mediator Pattern Diagram 1

Implementation

class ChatRoom : public ChatRoomMediator {
private:
    std::vector<User*> users_;

public:
    void addUser(User* user) override {
        users_.push_back(user);
        user->setChatRoom(this);
    }

    void sendMessage(const std::string& message, 
                    User* sender) override {
        for (auto* user : users_) {
            if (user != sender) {
                user->receive(message, sender);
            }
        }
    }

    void sendPrivateMessage(const std::string& message, 
                           User* from, User* to) override {
        to->receivePrivate(message, from);
    }
};

class User {
private:
    std::string name_;
    ChatRoomMediator* chatRoom_;

public:
    void send(const std::string& message) {
        if (chatRoom_) {
            chatRoom_->sendMessage(message, this);
        }
    }

    void receive(const std::string& message, User* from) {
        std::cout << name_ << " received: " << message 
                  << " from " << from->getName() << std::endl;
    }
};

// Usage
ChatRoom chatRoom;
User alice("Alice");
User bob("Bob");

chatRoom.addUser(&alice);
chatRoom.addUser(&bob);

alice.send("Hello everyone!");  // Bob receives it
bob.sendPrivate("Hi Alice!", &alice);  // Only Alice receives it

Real-World Example 2: Air Traffic Control

Mediator Pattern Diagram 2

Implementation

class AirportTower : public AirTrafficControl {
private:
    std::vector<Aircraft*> aircrafts_;
    bool runwayAvailable_;
    std::vector<Aircraft*> landingQueue_;
    std::vector<Aircraft*> takeoffQueue_;

public:
    AirportTower() : runwayAvailable_(true) {}

    bool requestLanding(Aircraft* aircraft) override {
        if (runwayAvailable_) {
            runwayAvailable_ = false;
            return true;
        } else {
            landingQueue_.push_back(aircraft);
            return false;
        }
    }

    bool requestTakeoff(Aircraft* aircraft) override {
        if (runwayAvailable_) {
            runwayAvailable_ = false;
            return true;
        } else {
            takeoffQueue_.push_back(aircraft);
            return false;
        }
    }

    void notifyRunwayClear(Aircraft* aircraft) override {
        runwayAvailable_ = true;

        // Landing has priority
        if (!landingQueue_.empty()) {
            Aircraft* next = landingQueue_.front();
            landingQueue_.erase(landingQueue_.begin());
            next->requestLanding();
        } else if (!takeoffQueue_.empty()) {
            Aircraft* next = takeoffQueue_.front();
            takeoffQueue_.erase(takeoffQueue_.begin());
            next->requestTakeoff();
        }
    }
};

class Aircraft {
private:
    std::string flightNumber_;
    AirTrafficControl* atc_;

public:
    void requestLanding() {
        if (atc_ && atc_->requestLanding(this)) {
            land();
        }
    }

    void clearRunway() {
        if (atc_) {
            atc_->notifyRunwayClear(this);
        }
    }
};

Real-World Example 3: GUI Dialog

Mediator Pattern Diagram 3

Implementation

class LoginDialog : public DialogMediator {
private:
    Textbox* usernameField_;
    Textbox* passwordField_;
    Checkbox* rememberMe_;
    Button* loginButton_;
    Button* clearButton_;

public:
    void notify(Component* component, 
                const std::string& event) override {
        if (component == loginButton_ && event == "click") {
            // Process login
            std::cout << "Logging in with:" << std::endl;
            std::cout << "  Username: " << usernameField_->getText() << std::endl;
            std::cout << "  Password: " << passwordField_->getText() << std::endl;
        } else if (component == clearButton_ && event == "click") {
            // Clear all fields
            usernameField_->clear();
            passwordField_->clear();
            rememberMe_->setChecked(false);
        } else if (event == "textChanged") {
            // Enable/disable login button
            bool hasUsername = !usernameField_->getText().empty();
            bool hasPassword = !passwordField_->getText().empty();
            // loginButton_->setEnabled(hasUsername && hasPassword);
        }
    }
};

Applicability

Use the Mediator pattern when:

  1. Complex communication between objects: A set of objects communicate in well-defined but complex ways. The resulting interdependencies are unstructured and difficult to understand.

  2. Difficult to reuse objects: Reusing an object is difficult because it refers to and communicates with many other objects.

  3. Customizable behavior: A behavior that's distributed between several classes should be customizable without a lot of subclassing.

  4. Too many relationships: An object needs to communicate with many other objects, creating a web of dependencies.

  5. Centralized control needed: You want to centralize complex coordination logic in one place.

Advantages

  1. Single Responsibility Principle: Extract communications between components into a single place, making it easier to understand and maintain.

  2. Open/Closed Principle: Introduce new mediators without changing actual components.

  3. Reduced Coupling: Components don't know about each other directly, reducing dependencies.

  4. Simplified Object Protocols: Replace many-to-many relationships with one-to-many relationships, which are easier to understand.

  5. Improved Reusability: Components can be reused more easily since they're not tied to specific colleagues.

  6. Centralized Control: Complex interaction logic is centralized in the mediator, making the system easier to understand.

Disadvantages

  1. God Object Risk: The mediator can evolve into a God Object that knows too much and does too much.

  2. Increased Complexity: Adds another layer of abstraction, which might be overkill for simple interactions.

  3. Single Point of Failure: The mediator becomes a single point of failure for the entire system.

  4. Performance Overhead: Indirect communication through mediator may have performance implications.

  5. Difficult to Maintain: A complex mediator can become difficult to maintain and understand.

Relations with Other Patterns

  • Facade: Facade is similar to Mediator in that it abstracts functionality of existing classes. However, Facade defines a simplified interface to a subsystem without adding new functionality. A Mediator centralizes communication between components, adding behavior.
  • Observer: The difference between Mediator and Observer is often elusive. In most cases, you can implement either pattern, but sometimes you can apply both simultaneously. The primary goal of Mediator is to eliminate mutual dependencies among components, while Observer is to establish dynamic one-way connections.
  • Chain of Responsibility: Can be used together with Mediator. A request can be passed along a chain of handlers, with a mediator coordinating the chain.
  • Command: Can be used to decouple senders and receivers. Commands can be stored and passed through a mediator.

Design Considerations

When to Use Mediator

  • Objects communicate in complex ways
  • Want to avoid tight coupling
  • Need centralized control
  • Behavior is distributed among many classes

When NOT to Use Mediator

  • Simple, straightforward communication
  • Performance is critical (direct communication is faster)
  • Would create an overly complex mediator
  • Objects naturally have direct relationships

Best Practices

  1. Keep mediator focused: Don't let it become a God Object
  2. Define clear interfaces: Between mediator and colleagues
  3. Consider two-way communication: Mediator notifies colleagues, colleagues notify mediator
  4. Use weak references: To avoid circular dependencies
  5. Document interaction protocols: Make it clear how components interact
  6. Consider event-driven approach: Use events/messages for flexibility

Comparison: Mediator vs Observer vs Event Bus

Aspect Mediator Observer Event Bus
Relationship One-to-many (bidirectional) One-to-many (unidirectional) Many-to-many
Coupling Low (through mediator) Medium (subject knows observers) Very low (anonymous)
Control Centralized Distributed Distributed
Use Case Complex interactions State changes Loose event handling

Real-World Applications

1. Chat Systems

  • Chat room coordinates all messages
  • Private and public messaging
  • User presence management

2. Air Traffic Control

  • Coordinates aircraft movements
  • Manages runway access
  • Maintains safety

3. GUI Frameworks

  • Dialog boxes coordinate widget interactions
  • Form validation
  • Enable/disable controls

4. Game Development

  • Game manager coordinates entities
  • Event system for entity communication
  • Collision detection

5. Smart Home Systems

  • Central hub coordinates devices
  • Automation rules
  • Device discovery and management

6. MVC Architecture

  • Controller mediates between Model and View
  • User input handling
  • Data updates and UI refresh

7. Workflow Engines

  • Orchestrates task execution
  • Manages task dependencies
  • Coordinates state transitions

Example: Smart Home System

class SmartHomeHub : public Mediator {
private:
    SmartLight* light_;
    SmartThermostat* thermostat_;
    MotionSensor* sensor_;

public:
    void notify(const std::string& message, 
                Colleague* sender) override {
        if (sender == sensor_ && message == "motion_detected") {
            light_->turnOn();
            thermostat_->setMode("comfort");
        } else if (sender == sensor_ && message == "no_motion") {
            light_->turnOff();
            thermostat_->setMode("eco");
        }
    }
};

Conclusion

The Mediator pattern is a powerful tool for managing complex interactions between objects. It:

  • Reduces coupling between components
  • Centralizes interaction logic
  • Simplifies component implementation
  • Improves maintainability and reusability

However, be cautious not to create an overly complex mediator. The pattern works best when: - Interactions are complex but well-defined - Components need to remain independent - You want centralized control over interactions - The system benefits from reduced coupling

When applied appropriately, the Mediator pattern significantly improves code organization and maintainability, making systems easier to understand, modify, and extend.