Mediator Pattern
The Mediator pattern is a behavioral design pattern that reduces coupling between objects by having them communicate through a mediator object. Instead of objects directly referencing each other, they communicate indirectly through the mediator. This pattern promotes loose coupling and improved maintainability by centralizing communication logic. This can simplify complex systems where objects interact in intricate ways.
What is Mediator Pattern
The Mediator pattern addresses the problem of tightly coupled objects within a system. When objects are directly connected, a change in one object can easily affect others, leading to a brittle and difficult-to-maintain system. The Mediator pattern solves this by:
- Decoupling: It decouples objects by introducing a mediator that acts as a central point of communication.
- Centralized Control: It centralizes communication logic, making it easier to understand and modify.
- Reduced Complexity: It reduces the complexity of the relationships between objects.
In-depth Explanation:
The pattern consists of two main parts:
- Mediator: An interface (or abstract class) that defines the contract for communication between colleague objects. It knows about all the colleagues and how to route messages between them.
- Colleague: An interface (or abstract class) that defines the behavior of objects that participate in the communication. Each colleague knows about the mediator but does not know about other colleagues.
Edge Cases:
- Over-Centralization: If the mediator becomes too complex, it can become a bottleneck and a single point of failure. Careful design is needed to ensure the mediator remains manageable.
- Simple Interactions: For very simple interactions, the Mediator pattern might be overkill. Direct communication might be more appropriate.
- Performance: Introducing a mediator can add a slight overhead to communication, as messages must be routed through the mediator. However, this is usually negligible compared to the benefits of decoupling.
Performance Considerations:
The performance overhead of the Mediator pattern is usually minimal. The mediator itself is typically a lightweight object, and the communication overhead is usually limited to a single function call. The benefits of decoupling and improved maintainability usually outweigh any potential performance concerns. However, in extremely performance-critical applications, itās important to profile the code to ensure that the mediator is not introducing a bottleneck.
Syntax and Usage
The basic structure of the Mediator pattern in C++ involves defining a Mediator interface and Colleague classes.
// Forward declaration
class Colleague;
class Mediator {
public:
virtual void sendMessage(const std::string& message, Colleague* colleague) = 0;
virtual void addColleague(Colleague* colleague) = 0;
virtual ~Mediator() = default;
};
class Colleague {
protected:
Mediator* mediator;
public:
Colleague(Mediator* mediator) : mediator(mediator) {}
virtual void sendMessage(const std::string& message) = 0;
virtual void receiveMessage(const std::string& message) = 0;
virtual ~Colleague() = default;
};To use it, you would create concrete Mediator and Colleague classes.
Basic Example
This example demonstrates a chat room scenario.
#include <iostream>
#include <string>
#include <vector>
// Forward declaration
class User;
class ChatMediator {
public:
virtual void sendMessage(const std::string& message, User* user) = 0;
virtual void addUser(User* user) = 0;
virtual ~ChatMediator() = default;
};
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) {
mediator->sendMessage(message, this);
}
virtual void receive(const std::string& message, const std::string& senderName) {
std::cout << name << " received: " << message << " from " << senderName << std::endl;
}
std::string getName() const { return name; }
virtual ~User() = default;
};
class ChatRoom : public ChatMediator {
private:
std::vector<User*> users;
public:
void sendMessage(const std::string& message, User* user) override {
for (User* u : users) {
if (u != user) {
u->receive(message, user->getName());
}
}
}
void addUser(User* user) override {
users.push_back(user);
}
};
class ConcreteUser : public User {
public:
ConcreteUser(ChatMediator* mediator, const std::string& name) : User(mediator, name) {}
};
int main() {
ChatRoom chatRoom;
ConcreteUser john(&chatRoom, "John");
ConcreteUser jane(&chatRoom, "Jane");
ConcreteUser peter(&chatRoom, "Peter");
chatRoom.addUser(&john);
chatRoom.addUser(&jane);
chatRoom.addUser(&peter);
john.send("Hello everyone!");
jane.send("Hi John!");
return 0;
}Explanation:
ChatMediatorandUserare abstract base classes for the mediator and colleagues.ChatRoomis a concrete mediator that manages the users.ConcreteUseris a concrete colleague.- The
mainfunction creates aChatRoomand severalConcreteUserobjects. - The users send messages through the
ChatRoom, which distributes them to the other users.
Advanced Example
This example demonstrates a slightly more complex scenario involving GUI components.
#include <iostream>
#include <string>
// Forward declarations
class Button;
class TextBox;
class Label;
class DialogMediator {
public:
virtual void notify(class Component* sender, const std::string& event) = 0;
virtual void registerComponent(class Component* component) = 0;
virtual ~DialogMediator() = default;
};
class Component {
protected:
DialogMediator* mediator;
public:
Component(DialogMediator* mediator) : mediator(mediator) {}
virtual ~Component() = default;
virtual void onClick() {}
virtual void onTextChanged(const std::string& text) {}
};
class Button : public Component {
private:
std::string label;
public:
Button(DialogMediator* mediator, const std::string& label) : Component(mediator), label(label) {}
void onClick() override {
std::cout << "Button '" << label << "' clicked." << std::endl;
mediator->notify(this, "click");
}
};
class TextBox : public Component {
private:
std::string text;
public:
TextBox(DialogMediator* mediator) : Component(mediator), text("") {}
void onTextChanged(const std::string& newText) override {
text = newText;
std::cout << "TextBox text changed to: " << text << std::endl;
mediator->notify(this, "text_changed");
}
std::string getText() const { return text; }
};
class Label : public Component {
private:
std::string text;
public:
Label(DialogMediator* mediator, const std::string& initialText) : Component(mediator), text(initialText) {
std::cout << "Label initialized with text: " << text << std::endl;
}
void setText(const std::string& newText) {
text = newText;
std::cout << "Label text changed to: " << text << std::endl;
}
};
class FontDialog : public DialogMediator {
private:
Button* okButton = nullptr;
Button* cancelButton = nullptr;
TextBox* fontNameTextBox = nullptr;
Label* previewLabel = nullptr;
public:
void setOkButton(Button* button) { okButton = button; }
void setCancelButton(Button* button) { cancelButton = button; }
void setFontNameTextBox(TextBox* textBox) { fontNameTextBox = textBox; }
void setPreviewLabel(Label* label) { previewLabel = label; }
void notify(Component* sender, const std::string& event) override {
if (sender == fontNameTextBox && event == "text_changed") {
if (previewLabel != nullptr) {
previewLabel->setText(fontNameTextBox->getText());
}
} else if (sender == okButton && event == "click") {
std::cout << "OK Button Clicked: Applying Font " << fontNameTextBox->getText() << std::endl;
} else if (sender == cancelButton && event == "click") {
std::cout << "Cancel Button Clicked: Discarding Changes" << std::endl;
}
}
void registerComponent(Component* component) override {
if (Button* button = dynamic_cast<Button*>(component)) {
if(button->label == "OK"){
setOkButton(button);
} else if (button->label == "Cancel"){
setCancelButton(button);
}
} else if (TextBox* textBox = dynamic_cast<TextBox*>(component)) {
setFontNameTextBox(textBox);
} else if (Label* label = dynamic_cast<Label*>(component)) {
setPreviewLabel(label);
}
}
};
int main() {
FontDialog dialog;
Button okButton(&dialog, "OK");
Button cancelButton(&dialog, "Cancel");
TextBox fontNameTextBox(&dialog);
Label previewLabel(&dialog, "Initial Font");
dialog.registerComponent(&okButton);
dialog.registerComponent(&cancelButton);
dialog.registerComponent(&fontNameTextBox);
dialog.registerComponent(&previewLabel);
fontNameTextBox.onTextChanged("Arial");
okButton.onClick();
cancelButton.onClick();
return 0;
}Common Use Cases
- GUI Frameworks: Managing interactions between UI elements.
- Chat Applications: Routing messages between users.
- Event Management Systems: Handling events and notifying subscribers.
- Workflow Systems: Coordinating tasks between different components.
Best Practices
- Keep the Mediator Simple: Avoid making the mediator too complex. Delegate complex logic to the colleague objects if possible.
- Use Interfaces: Define the mediator and colleague interfaces to allow for flexibility and extensibility.
- Consider Observer Pattern: In some cases, a combination of Mediator and Observer patterns can be useful.
Common Pitfalls
- Overuse: Donāt use the Mediator pattern for simple interactions.
- Mediator as a God Object: Avoid making the mediator responsible for too much logic.
- Tight Coupling to Concrete Classes: Avoid tight coupling between the mediator and concrete colleague classes.
Key Takeaways
- The Mediator pattern reduces coupling between objects.
- It centralizes communication logic.
- It improves maintainability and testability.
- It should be used judiciously, as it can introduce overhead.