The Mediator Design Pattern is a behavioral design pattern that defines an object, called the mediator, to encapsulate how a set of objects interact. This pattern promotes loose coupling by preventing objects from referring to each other directly. Instead, they communicate via the mediator.
This design pattern states that 2 objects will never interact directly. They should interact via mediator.
Example: chat application, Airline management system, Auction system.
This approach decouples objects, making the system more modular, easier to extend, and maintainable.
Problem
When multiple objects needs to interact, they establish many-to-may relationships. This creates a web of connections.
It becomes increasingly difficult to trace or debug these interactions as the system grows.
Example Problem: If A, B, C and D
need to communicate, without a mediator, you might end up with code like this:
class A {
public:
void notifyB(B* b) { b->doSomething(); }
void notifyC(C* c) { c->doSomething(); }
};
class B {
public:
void notifyD(D* d) { d->doSomething(); }
};
// Similar dependencies for C and D...
Here, every object is aware of other objects and directly interacts with them. This can lead to cascading changes when new objects are added or existing ones are modified.
+----------+ +----------+
| Object A |<---------->| Object C |
+----------+ \ +----------+
^ \ / ^
| \ / |
| \ / |
| \ / |
| /\ |
| / \ |
v / \ v
+----------+ / +----------+
| Object B |<------->| Object D |
+----------+ +----------+
- Adding new components requires modifying existing classes to accommodate the new interactions.
- The complexity grows exponentially as the number of objects increases.
For example:
- With 2 objects, there’s 1 possible communication line.
- With 3 objects, there are 3 possible communication lines.
- With 4 objects, there are 6 lines, and so on.
In general, the number of connections = n × (n - 1) / 2, where n
is the number of objects. For larger systems, this becomes unmanageable.
Consider a chat application where users communicate directly with each other.
class User {
public:
std::string name;
User(const std::string& name) : name(name) {}
void sendMessage(User* receiver, const std::string& message) {
std::cout << name << " sends message to " << receiver->name << ": " << message << std::endl;
receiver->receiveMessage(this, message);
}
void receiveMessage(User* sender, const std::string& message) {
std::cout << name << " receives message from " << sender->name << ": " << message << std::endl;
}
};
int main() {
User* alice = new User("Alice");
User* bob = new User("Bob");
alice->sendMessage(bob, "Hello, Bob!");
bob->sendMessage(alice, "Hi, Alice!");
delete alice;
delete bob;
return 0;
}
In this scenario:
- If a new user (e.g., "Charlie") is added, every other user may need modifications to interact with them.
- The direct dependency between users makes scaling difficult as the application grows.
Here is the chat application from before, refactored using the Mediator Pattern
.
+----------+ +----------+
| Object A | | Object C |
+----------+ \ +----------+
\ /
\ /
\ /
+----------+
| Mediator |
+----------+
/ \
/ \
+----------+ / +----------+
| Object B | | Object D |
+----------+ +----------+
#include <iostream>
#include <string>
#include <vector>
// Mediator Interface
class Mediator {
public:
virtual void sendMessage(const std::string& message, class User* sender) = 0;
virtual void addUser(class User* user) = 0;
};
// Abstract Colleague (User)
class User {
protected:
Mediator* mediator;
std::string name;
public:
User(Mediator* mediator, const std::string& name) : mediator(mediator), name(name) {}
virtual void send(const std::string& message) = 0;
virtual void receive(const std::string& message) = 0;
std::string getName() const { return name; }
};
// Concrete Mediator (ChatRoom)
class ChatRoom : public Mediator {
private:
std::vector<User*> users; // Maintains a list of users in the chatroom
public:
void addUser(User* user) override {
users.push_back(user);
}
void sendMessage(const std::string& message, User* sender) override {
for (User* user : users) {
if (user != sender) { // Do not send the message back to the sender
user->receive(message);
}
}
}
};
// Concrete Colleague (ConcreteUser)
class ConcreteUser : public User {
public:
ConcreteUser(Mediator* mediator, const std::string& name) : User(mediator, name) {}
void send(const std::string& message) override {
std::cout << name << " sends: " << message << std::endl;
mediator->sendMessage(message, this);
}
void receive(const std::string& message) override {
std::cout << name << " receives: " << message << std::endl;
}
};
// Main Function
int main() {
// Create a mediator (chat room)
ChatRoom* chatRoom = new ChatRoom();
// Create users (colleagues)
User* alice = new ConcreteUser(chatRoom, "Alice");
User* bob = new ConcreteUser(chatRoom, "Bob");
User* charlie = new ConcreteUser(chatRoom, "Charlie");
// Add users to the chat room
chatRoom->addUser(alice);
chatRoom->addUser(bob);
chatRoom->addUser(charlie);
// Users send messages via the mediator
alice->send("Hello, everyone!");
bob->send("Hi Alice!");
charlie->send("Good morning!");
// Cleanup
delete alice;
delete bob;
delete charlie;
delete chatRoom;
return 0;
}
Alice sends: Hello, everyone!
Bob receives: Hello, everyone!
Charlie receives: Hello, everyone!
Bob sends: Hi Alice!
Alice receives: Hi Alice!
Charlie receives: Hi Alice!
Charlie sends: Good morning!
Alice receives: Good morning!
Bob receives: Good morning!
UML:
Interface: Mediator
--------------------
+-----------------------------+
| Mediator |
+-----------------------------+
| + sendMessage(message: string, sender: User): void |
+-----------------------------+
Class: ChatRoom (ConcreteMediator)
----------------------------------
+-----------------------------+
| ChatRoom |
+-----------------------------+
| - users: List<User> |
+-----------------------------+
| + addUser(user: User): void |
| + sendMessage(message: string, sender: User): void |
+-----------------------------+
Abstract Class: User (Colleague)
--------------------------------
+-----------------------------+
| User |
+-----------------------------+
| # mediator: Mediator |
| # name: string |
+-----------------------------+
| + send(message: string): void |
| + receive(message: string): void |
+-----------------------------+
Class: ConcreteUser (ConcreteColleague)
---------------------------------------
+-----------------------------+
| ConcreteUser |
+-----------------------------+
| (Inherits: User) |
+-----------------------------+
| + send(message: string): void |
| + receive(message: string): void |
+-----------------------------+
Relationships
-------------
1. **Mediator → ConcreteMediator**:
- ChatRoom implements Mediator (dashed arrow with "implements").
2. **User → ConcreteUser**:
- ConcreteUser inherits User (dashed arrow with "inherits").
3. **ChatRoom ↔ User**:
- ChatRoom maintains a list of Users (association with a solid line).
- Users interact with ChatRoom to send/receive messages (solid line with dependency).
Intent
- Simplify communication between objects by centralizing control.
- Reduce dependencies between objects, promoting a more maintainable and decoupled design.
Key Components
- Mediator (Interface):
- Defines the interface for communication between colleague objects.
- Concrete Mediator:
- Implements the mediator interface and coordinates the communication logic between colleagues.
- Colleague:
- Individual components or objects that interact via the mediator.
- Each colleague holds a reference to the mediator but does not directly interact with other colleagues.
When to Use
- When the system has many interacting objects, leading to complex interconnections (spaghetti code).
- To make the system easier to extend by introducing new colleagues without modifying existing ones.
- To improve maintainability by reducing the number of dependencies between objects.
Benefits of the Mediator Design Pattern
- Loose Coupling:
- Objects no longer depend directly on each other, improving modularity.
- Improved Maintainability:
- Changes in communication logic are confined to the mediator, reducing the risk of cascading changes.
- Simplified Communication:
- The mediator handles interactions, eliminating many-to-many relationships.
- Scalability:
- Adding new components or modifying interactions becomes easier.
https://github.com/ashishps1/awesome-system-design-resources
https://github.com/ashishps1?tab=repositories
Implementation
Example: Chat Room
In a chat room, users send messages to one another, but instead of communicating directly, they go through the chat room (mediator).
#include <iostream>
#include <string>
#include <vector>
// Mediator Interface
class ChatMediator {
public:
virtual void sendMessage(const std::string& message, class User* user) = 0;
};
// Colleague Class
class User {
protected:
ChatMediator* mediator;
std::string name;
public:
User(ChatMediator* mediator, const std::string& name) : mediator(mediator), name(name) {}
virtual void send(const std::string& message) = 0;
virtual void receive(const std::string& message) = 0;
std::string getName() const { return name; }
};
// Concrete Mediator
class ChatRoom : public ChatMediator {
private:
std::vector<User*> users;
public:
void addUser(User* user) {
users.push_back(user);
}
void sendMessage(const std::string& message, User* sender) override {
for (User* user : users) {
if (user != sender) {
user->receive(message);
}
}
}
};
// Concrete Colleague
class ConcreteUser : public User {
public:
ConcreteUser(ChatMediator* mediator, const std::string& name) : User(mediator, name) {}
void send(const std::string& message) override {
std::cout << name << " sends: " << message << std::endl;
mediator->sendMessage(message, this);
}
void receive(const std::string& message) override {
std::cout << name << " receives: " << message << std::endl;
}
};
// Main Function
int main() {
ChatRoom* chatRoom = new ChatRoom();
User* alice = new ConcreteUser(chatRoom, "Alice");
User* bob = new ConcreteUser(chatRoom, "Bob");
User* charlie = new ConcreteUser(chatRoom, "Charlie");
chatRoom->addUser(alice);
chatRoom->addUser(bob);
chatRoom->addUser(charlie);
alice->send("Hello, everyone!");
bob->send("Hey, Alice!");
delete alice;
delete bob;
delete charlie;
delete chatRoom;
return 0;
}
Output:
Alice sends: Hello, everyone!
Bob receives: Hello, everyone!
Charlie receives: Hello, everyone!
Bob sends: Hey, Alice!
Alice receives: Hey, Alice!
Charlie receives: Hey, Alice!