Mediator Design Pattern: Building a Chat Room
The Mediator Design Pattern is a behavioral design pattern that simplifies chaotic communication between multiple objects. Instead of allowing objects to communicate directly with one another—which creates a messy, tightly coupled web of dependencies—the Mediator pattern introduces a single central hub that manages all interactions, promoting loose coupling and maintainability.
1. Why Do We Need This? (The Problem of Tight Coupling)
Imagine you are building a collaborative network or an online chat room where multiple objects (OB1, OB2, OB3, etc.) need to talk to each other. In an object-oriented paradigm, talking means calling methods on other objects.
To allow OB1 to talk to OB2, OB1 must hold a reference to OB2. If a third object OB3 is introduced, OB1 must now store a reference to OB3, OB2 must store references to both OB1 and OB3, and OB3 must store references to OB1 and OB2.
The Scaling Math of Direct Communication
As you introduce new objects into a network, the number of reference lists and direct connections explodes:
Objects in Network: N
Required Connections per Object: N - 1
Total Inter-object Connections: O(N^2)
- Memory Overhead: Every single object must maintain a collection (vector/map) of every other object in the system.
- Extreme Tight Coupling: If you modify a user class or add a feature (such as blocking or mutting), you have to ripple changes across every single object's internal list logic.
- Maintenance Nightmare: Adding, removing, or debugging participants in the network becomes exceptionally fragile.
2. Core Mental Model & The Mediator Solution
Real-Life Analogy: Air Traffic Control (ATC)
Think of an airport with 10 different airplanes trying to land and take off.
- Without Mediator: Every pilot must talk directly to every other pilot in the air to coordinate landing times, speeds, and runways. This would result in catastrophic chaos.
- With Mediator (Air Traffic Control Tower): None of the pilots talk to each other directly. Every pilot talks only to the ATC Tower. The ATC Tower maintains the master schedule and routes messages safely.
Applying the Mental Model to Code
By routing all communication through a central Mediator object:
- Zero Peer References: Individual objects no longer need to know about or store references to other peers.
- Single Point of Control: Only the Mediator stores the master collection of participants.
- Loose Coupling: Objects only depend on the Mediator interface, making components easily reusable and testable.
3. Naive Implementation (Without Mediator Pattern)
Let's examine how a chat room looks when implemented naively without a Mediator. Every User class holds a list of all other users and manages its own broadcast and private messaging logic.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
class User {
private:
std::string name;
std::vector<User*> peers; // List of all other users in network
std::vector<std::string> mutedUsers; // List of muted user names
public:
User(std::string n) : name(n) {}
std::string getName() const { return name; }
// Adds a peer connection (O(N^2) wiring overhead)
void addPeer(User* peer) {
peers.push_back(peer);
}
void mute(std::string userName) {
mutedUsers.push_back(userName);
}
bool isMuted(std::string userName) const {
for (const auto& m : mutedUsers) {
if (m == userName) return true;
}
return false;
}
// Broadcast message to all peers (unless muted)
void sendAll(std::string message) {
std::cout << "[" << name << "] Broadcasting: " << message << "\n";
for (auto* peer : peers) {
if (!peer->isMuted(name)) {
peer->receive(name, message);
}
}
}
// Send private message to specific user
void sendTo(std::string targetName, std::string message) {
std::cout << "[" << name << "] Sending private to " << targetName << ": " << message << "\n";
for (auto* peer : peers) {
if (peer->getName() == targetName) {
if (!peer->isMuted(name)) {
peer->receive(name, message);
}
break;
}
}
}
void receive(std::string from, std::string message) {
std::cout << name << " got message from " << from << ": " << message << "\n";
}
};
The Pitfalls of the Naive Approach
- Complex Wiring in
main: To wire up 3 users (Rohan,Neha,Mohan), you have to manually cross-link every single user's peer vector:C++rohan->addPeer(neha); neha->addPeer(rohan); rohan->addPeer(mohan); mohan->addPeer(rohan); neha->addPeer(mohan); mohan->addPeer(neha); - Feature Bloat: Adding a feature like
mute()forces every user object to maintain secondary tracking lists (mutedUsers), complicating checks inside every message dispatch.
4. Step-by-Step Evolution: Introducing the Mediator
To resolve the structural coupling, we decouple the chat participants (called Colleagues) from one another and introduce a ChatMediator.
Architectural Flow with Mediator
5. Complete Refactored Code (With Mediator Pattern)
Here is the clean, decoupled production-grade implementation of the Chat Room using the Mediator Design Pattern in C++.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
// Forward Declarations
class IMediator;
// ==========================================
// 1. Abstract Colleague Class
// ==========================================
class Colleague {
protected:
IMediator* mediator;
std::string name;
public:
Colleague(IMediator* m, std::string n) : mediator(m), name(n) {}
virtual ~Colleague() = default;
std::string getName() const { return name; }
virtual void send(std::string message) = 0;
virtual void sendPrivate(std::string to, std::string message) = 0;
virtual void receive(std::string from, std::string message) = 0;
};
// ==========================================
// 2. Abstract Mediator Interface
// ==========================================
class IMediator {
public:
virtual ~IMediator() = default;
virtual void registerColleague(Colleague* colleague) = 0;
virtual void sendMessage(std::string from, std::string message) = 0;
virtual void sendPrivateMessage(std::string from, std::string to, std::string message) = 0;
};
// ==========================================
// 3. Concrete Mediator (Chat Room Hub)
// ==========================================
class ChatMediator : public IMediator {
private:
std::vector<Colleague*> colleagues;
// Store mute pairs: { Who Muted, Who Is Muted }
std::vector<std::pair<std::string, std::string>> mutes;
public:
void registerColleague(Colleague* colleague) override {
colleagues.push_back(colleague);
}
void muteUser(std::string who, std::string whom) {
mutes.push_back({who, whom});
}
bool isMuted(std::string sender, std::string receiver) const {
for (const auto& pair : mutes) {
// pair.first = who muted, pair.second = who is muted
if (pair.first == receiver && pair.second == sender) {
return true;
}
}
return false;
}
void sendMessage(std::string from, std::string message) override {
std::cout << "[ChatRoom] Broadcast from " << from << ": " << message << "\n";
for (auto* c : colleagues) {
if (c->getName() == from) continue; // Don't send back to sender
if (!isMuted(from, c->getName())) {
c->receive(from, message);
} else {
std::cout << " -> (Blocked by mute: " << c->getName() << " muted " << from << ")\n";
}
}
}
void sendPrivateMessage(std::string from, std::string to, std::string message) override {
std::cout << "[ChatRoom] Private from " << from << " to " << to << ": " << message << "\n";
for (auto* c : colleagues) {
if (c->getName() == to) {
if (!isMuted(from, c->getName())) {
c->receive(from, message);
} else {
std::cout << " -> Message blocked. " << to << " has muted " << from << ".\n";
}
return;
}
}
}
};
// ==========================================
// 4. Concrete Colleague (User Class)
// ==========================================
class User : public Colleague {
public:
User(IMediator* m, std::string n) : Colleague(m, n) {
mediator->registerColleague(this); // Auto-register upon creation
}
void send(std::string message) override {
mediator->sendMessage(name, message);
}
void sendPrivate(std::string to, std::string message) override {
mediator->sendPrivateMessage(name, to, message);
}
void receive(std::string from, std::string message) override {
std::cout << " [Inbox - " << name << "] Message from " << from << " -> " << message << "\n";
}
};
// ==========================================
// 5. Execution Demo (Main)
// ==========================================
int main() {
ChatMediator* chatRoom = new ChatMediator();
// Users automatically register with the chat room
User* rohan = new User(chatRoom, "Rohan");
User* neha = new User(chatRoom, "Neha");
User* mohan = new User(chatRoom, "Mohan");
// Rohan mutes Mohan
chatRoom->muteUser("Rohan", "Mohan");
// Rohan broadcasts a message
std::cout << "\n--- Test 1: Broadcast ---" << std::endl;
rohan->send("Hello Everyone!");
// Mohan tries to send a private message to Neha
std::cout << "\n--- Test 2: Private Message ---" << std::endl;
mohan->sendPrivate("Neha", "Hey Neha, can you hear me?");
// Cleanup memory
delete rohan;
delete neha;
delete mohan;
delete chatRoom;
return 0;
}
6. Mediator vs. Observer Design Pattern (Crucial Interview Distinction)
Both patterns feature a central hub managing a collection of objects, which often confuses engineers. Here is the exact distinction based on Intent:
7. Real-World Practical Use Cases
- Chat Room Applications: Slack channels, Discord servers, and WhatsApp groups where users talk to the room/mediator rather than direct socket-to-socket wiring.
- Matchmaking & Gaming Lobbies: Multiplayer game rooms (
Chess.com,Ludo King) where players' moves and actions are mediated by a game server lobby. - GUI Dialog Forms: A dialog box containing buttons, checkboxes, and input fields. Instead of every UI control listening to and modifying every other control directly, the Dialog form acts as a Mediator controlling state interactions.
8. Summary and Key Takeaways
- Problem Solver: Eliminates direct inter-object connections and tight coupling in multi-object networks.
- Centralized Hub: The Mediator encapsulates all communication protocols and routing rules in one place.
- Loose Coupling: Colleagues only depend on the
IMediatorinterface and remain completely blind to each other's existence or references. - Simplified Maintenance: Adding new features (such as muting, broadcasting, logging, or permissions) only requires updating the Mediator class rather than every individual peer.
- Intent Matters: Do not confuse with the Observer pattern; Observer is for state-change broadcasting, whereas Mediator is for peer-to-peer conversation routing.