Design patterns provide us with well-structured and reusable solutions to recurring problems. Today, we’ll explore one such pattern that plays a crucial role in managing complex interactions between objects—the Mediator Design Pattern. In this blog, I’ll guide you through its concept, benefits, and how to implement it in Kotlin. By the end, you’ll have a clear and solid understanding of the Mediator Design Pattern.
What is the Mediator Design Pattern?
The Mediator Design Pattern simplifies communication between multiple objects by introducing a mediator object that acts as a central hub. Instead of objects directly referencing each other, they interact through the mediator. This reduces dependencies and makes the code more modular and easier to manage.
While both the Mediator and Observer patterns involve communication between objects, the key difference is in how they handle it. In the Observer pattern, a subject notifies its observers whenever it changes, leading to direct communication between the subject and its observers. In contrast, the Mediator Pattern centralizes communication, where objects (colleagues) send messages to a mediator instead of directly interacting with each other. The mediator then coordinates and notifies the relevant colleagues about changes.
Think of it like a project manager in a team. Team members don’t communicate directly for every decision; instead, the project manager coordinates their interactions. This reduces chaos and improves collaboration.
When Should We Use the Mediator Design Pattern?
When designing reusable components, tight dependencies between them can lead to tangled, “spaghetti-like” code. In this situation, reusing individual classes becomes difficult because they are too interconnected. It’s like trying to remove one piece from a tangled heap—you either end up taking everything or nothing at all.
Spaghetti Code Analogy: Imagine a string of Christmas lights where each bulb is directly wired to the next. If one bulb is faulty or needs to be replaced, you can’t just swap out that single bulb. Since all the bulbs are tightly connected, replacing one requires adjusting or replacing the entire string. This is similar to spaghetti code, where components are so tightly coupled that isolating one to make changes without affecting others becomes very difficult.
Solution with the Mediator Pattern: Now, imagine instead that each bulb is connected to a central controller (the mediator). If one bulb needs to be replaced or updated, the controller handles the communication between bulbs. The bulbs no longer interact with each other directly. Instead, all communication goes through the mediator. This way, the rest of the system remains unaffected by changes to a single bulb, and the system becomes more modular with fewer dependencies between components.
We should consider using the Mediator Pattern when:
- Multiple objects must interact in complex ways.
- Tight coupling between objects makes the system difficult to maintain or extend.
- Changes in one component should not cascade through the entire system, causing ripple effects.
Structure of Mediator Design Pattern
Mediator Interface
- Defines a contract for communication between components.
Concrete Mediator
- Implements the Mediator interface and manages the communication between components.
Colleague (Component)
- Represents the individual components that interact with each other via the mediator.
Concrete Colleague
- Implements the specific behavior of a component.
The Mediator design pattern is structured to centralize communication and decouple interacting objects. Here’s how it works:
Centralized Communication: All communication between objects (known as colleagues) is routed through a central mediator. This ensures that each object doesn’t need to be aware of the others, and all interactions are coordinated in one place.
Decoupling of Objects: The colleague objects don’t communicate with each other directly. Instead, they send messages through the mediator, which handles the communication. This reduces the complexity of managing direct dependencies between objects. For example, in an air traffic control system, instead of planes communicating directly with one another, they interact with the air traffic controller (the mediator). The controller manages the planes’ interactions, ensuring safe, efficient, and orderly communication, preventing collisions or miscommunication.
Project Manager and Team Communication
If you remember, we discussed a real-world example earlier—Project Manager and Team. Now, let’s implement the Mediator design pattern for their communication.
Define the Mediator Interface
This interface allows the mediator to facilitate communication between colleagues.
// The Mediator interface defines how colleagues communicate via the mediator
interface Mediator {
fun sendMessage(message: String, colleague: Colleague)
}
Create the Concrete Mediator
The ProjectManager
acts as the mediator.
// Concrete Mediator (Project Manager) that implements the Mediator interface
class ProjectManager : Mediator {
private val colleagues = mutableListOf<Colleague>()
// Method to register colleagues (team members)
fun addColleague(colleague: Colleague) {
colleagues.add(colleague)
}
// The mediator routes the messages between colleagues
override fun sendMessage(message: String, colleague: Colleague) {
// Forward the message to all other colleagues, but not to the sender
colleagues.forEach {
if (it != colleague) {
it.receiveMessage(message, colleague.name)
}
}
}
}
Define the Colleague Abstract Class
This represents participants that communicate through the mediator.
// The Colleague class represents a team member who communicates through the mediator
abstract class Colleague(protected val mediator: Mediator, val name: String) {
// Send a message through the mediator
abstract fun sendMessage(message: String)
// Receive a message from the mediator
abstract fun receiveMessage(message: String, sender: String)
}
Create Concrete Colleagues
The team members are the concrete colleagues.
// Concrete Colleague (Team Member) classes representing individual team members
class TeamMember(mediator: Mediator, name: String) : Colleague(mediator, name) {
// Implementing the sendMessage method, sends message through the mediator
override fun sendMessage(message: String) {
println("$name sends message: \"$message\"")
mediator.sendMessage(message, this)
}
// Implementing the receiveMessage method, where messages are received via the mediator
override fun receiveMessage(message: String, sender: String) {
println(" -> $name received message: \"$message\" from $sender")
}
}
Demonstrate the Pattern
Here’s how it all comes together in the main function.
// Main function to run the simulation
fun main() {
// Create a mediator (Project Manager)
val projectManager = ProjectManager()
// Create team members (colleagues)
val akshay = TeamMember(projectManager, "Akshay")
val ria = TeamMember(projectManager, "Ria")
val amol = TeamMember(projectManager, "Amol")
// Register team members with the mediator
projectManager.addColleague(akshay)
projectManager.addColleague(ria)
projectManager.addColleague(amol)
// Communication through the mediator
println("--- Communication Flow ---")
akshay.sendMessage("Ria, have you completed the feature Alpha-Approval?") // Akshay sends a message to Ria
ria.sendMessage("Yes, Akshay. I'll demo it soon.") // Ria replies to Akshay
amol.sendMessage("Hey team, are there any blockers?") // Amol sends a message to the team
akshay.sendMessage("No blockers from my side, Amol.") // Akshay responds to Amol
ria.sendMessage("Same here, ready to deploy.") // Ria responds to Amol
}
Output
--- Communication Flow ---
Akshay sends message: "Ria, have you completed the feature Alpha-Approval?"
-> Ria received message: "Ria, have you completed the feature Alpha-Approval?" from Akshay
-> Amol received message: "Ria, have you completed the feature Alpha-Approval?" from Akshay
Ria sends message: "Yes, Akshay. I'll demo it soon."
-> Akshay received message: "Yes, Akshay. I'll demo it soon." from Ria
-> Amol received message: "Yes, Akshay. I'll demo it soon." from Ria
Amol sends message: "Hey team, are there any blockers?"
-> Akshay received message: "Hey team, are there any blockers?" from Amol
-> Ria received message: "Hey team, are there any blockers?" from Amol
Akshay sends message: "No blockers from my side, Amol."
-> Ria received message: "No blockers from my side, Amol." from Akshay
-> Amol received message: "No blockers from my side, Amol." from Akshay
Ria sends message: "Same here, ready to deploy."
-> Akshay received message: "Same here, ready to deploy." from Ria
-> Amol received message: "Same here, ready to deploy." from Ria
We implemented the Mediator design pattern using a Project Manager as the central point of coordination. In a typical team setup, communication and updates would flow directly through the Project Manager. But after the COVID-19 pandemic, team members started working remotely from different locations, and that’s when communication shifted to platforms like chatrooms, WhatsApp groups, Slack, or other tools.
In this new setup, these platforms essentially became the mediators. They ensure that all communication flows through a central hub, which eliminates the need for team members to communicate directly with each other. This helps keep things organized and reduces confusion.
With this approach, using a generalized implementation of the Mediator design pattern (with the Project Manager as an example), messages are now clearly formatted, making it easier to track who sent the message and who received it. Symbols, like arrows (->), show the flow of messages from the sender to the recipients. This layout gives us a clear, visual representation of how the communication happens through the mediator.
Benefits of Using the Mediator Pattern
- Reduced Complexity: By centralizing interactions, we eliminate the need for multiple direct references.
- Improved Flexibility: Adding or modifying components becomes easier as they are only dependent on the mediator.
- Enhanced Maintainability: The mediator encapsulates the interaction logic, making it easier to manage.
Potential Drawbacks
- Single Point of Failure: The mediator can become a bottleneck or overly complex if not designed well.
- Overhead: For simple scenarios, using a mediator may introduce unnecessary indirection.
Conclusion
The Mediator design pattern is a fantastic way to manage complex interactions in a decoupled, organized manner. Implementing it in Kotlin is both simple and powerful, allowing you to streamline communication by centralizing it through a mediator. This approach leads to cleaner, more maintainable, and easily extendable systems.
I hope this blog has helped clear up the Mediator pattern for you. If you’re working on a Kotlin project, give it a try—it’s a great way to simplify your codebase.
What’s awesome about Kotlin is that the Mediator pattern fits seamlessly with advanced concepts like coroutines or dependency injection, giving you even more power and flexibility. Of course, it’s important to weigh the pattern’s benefits against potential downsides, making sure it aligns with your project’s needs.
Let’s keep learning and building amazing things together!
Happy coding! 😊