1. Introduction

One of the challenges developers often face is managing the communication between various components in a system. Indeed, this is where design patterns come into play, offering proven solutions to recurring design problems. Specifically, a design pattern is a general reusable solution to a commonly occurring problem within a given context in software design.

In this tutorial, we’ll delve into the mediator pattern, which is a behavioral design pattern, and explore its implementation in Kotlin.

2. What Is the Mediator Pattern?

The Mediator Pattern promotes loose coupling by centralizing communication between objects, thus avoiding direct connections between them. Instead of components communicating directly, they communicate through a mediator object. Additionally, this mediator encapsulates the interaction logic, allowing components to be independent and unaware of each other.

In the mediator pattern, specifically, the key components work collaboratively to achieve a decoupled and organized system. Let’s delve deeper into the roles and responsibilities of each component.

2.1. Mediator

The mediator serves as the central hub in the mediator pattern, acting as a facilitator for communication and coordination among the various components. Its primary responsibilities include:

  • Acting as a communication interface that all participants adhere to
  • Tracking references to all current participants to facilitate communication between them
  • Coordinating the flow of information between participants without direct dependencies between individual components

These responsibilities allow the mediator to be the central point of control in this pattern. Indeed, by centralizing control, the mediator promotes a more maintainable and extensible system. Additionally, changes in communication logic or the addition of new components can be handled within the mediator, reducing the impact on individual components.

2.2. Colleague

A colleague is any component within the system that interacts with others through the mediator. Its role involves:

  • Maintaining a reference to the mediator to allow colleagues to send and receive messages without directly referencing other colleagues
  • Sending and receiving messages through the mediator to support the flow of information through the system

This allows colleagues to actively participate by responding to messages and executing actions, all while contributing to the overall behavior of the system without caring about intricate connections with other components.

2.3. Collaboration Dynamics

The mediator pattern establishes a many-to-many relationship among colleagues, allowing for flexible and dynamic collaboration. Colleagues can communicate with each other indirectly through the mediator, promoting a modular and scalable architecture. For this reason, the pattern enhances the maintainability, extensibility, and testability of complex systems by encapsulating communication logic within the mediator, minimizing the dependencies between individual components.

3. Implementation in Kotlin

In this section, we’ll implement the mediator pattern in Kotlin.

3.1. Tightly Coupled Example Without the Mediator Pattern

We’re going to introduce a tightly coupled scenario in our Kotlin code that we’ll later solve using the mediator pattern:

class Airplane(val registrationNumber: String) {
    private val otherAirplanes: MutableList<Airplane> = mutableListOf()
    fun addAirplane(airplane: Airplane) {
        otherAirplanes.add(airplane)
    }
    fun removeAirplane(airplane: Airplane) {
        otherAirplanes.remove(airplane)
    }
    fun takeoff() {
        println("$registrationNumber is taking off.")
        otherAirplanes.forEach { it.removeAirplane(this) }
        otherAirplanes.clear()
    }
    fun land(otherAirplanes: List<Airplane>) {
        println("$registrationNumber is landing.")
        this.otherAirplanes.addAll(otherAirplanes)
        otherAirplanes.forEach { it.addAirplane(this) }
    }
}

In this example, the takeoff() method demonstrates direct communication between airplanes. It iterates over the list of other airplanes and calls their removeAirplane() method, essentially notifying each of them about the takeoff. This creates a tight coupling between airplanes as each airplane needs to know about others and their methods.

Similarly, the land() method establishes direct communication with other airplanes notifying them about the landing by calling their addAirplane() method.

This direct communication creates a tight coupling between Airplane objects as each one needs to know about the existence and behavior of others.

3.2. Implementation of the Mediator Pattern

Let’s see how we can approach the same scenario using the mediator pattern to keep our code scalable:

interface AirTrafficController {
    fun registerAirplane(airplane: Airplane)
    fun deregisterAirplane(airplane: Airplane)
    fun requestTakeOff(airplane: Airplane)
    fun requestLanding(airplane: Airplane)
}
class Airplane(private val registrationNumber: String, private val controller: AirTrafficController) {
    fun takeOff() {
        println("$registrationNumber is requesting takeoff.")
        controller.requestTakeOff(this)
    }
    fun land() {
        println("$registrationNumber is requesting landing.")
        controller.requestLanding(this)
    }
    fun notifyTakeOff() {
        println("$registrationNumber has taken off.")
        controller.deregisterAirplane(this)
    }
    fun notifyLanding() {
        println("$registrationNumber has landed.")
        controller.registerAirplane(this)
    }
}

Now that we’ve introduced the colleague class and mediator interface, let’s look at a concrete implementation for our mediator:

class AirTrafficControlTower : AirTrafficController {
    private val registeredAirplanes: MutableSet<Airplane> = mutableSetOf()
    override fun registerAirplane(airplane: Airplane) {
        registeredAirplanes.add(airplane)
    }
    override fun deregisterAirplane(airplane: Airplane) {
        registeredAirplanes.remove(airplane)
    }
    override fun requestTakeOff(airplane: Airplane) {
        if (registeredAirplanes.contains(airplane)) {
            airplane.notifyTakeOff()
        }
    }
    override fun requestLanding(airplane: Airplane) {
        if (!registeredAirplanes.contains(airplane)) {
            airplane.notifyLanding()
        }
    }
} 

In this example, the Airplane objects don’t directly communicate with each other. Instead, they communicate through the AirTrafficController interface. The AirTrafficControlTower then implements the AirTrafficController interface and handles the communication between airplanes.

The Airplane class registers and deregisters itself with the AirTrafficController and uses the controller to request takeoff and landing. The controller decides whether to grant or deny these requests.

This example demonstrates the decoupling of airplanes, allowing them to communicate through the mediator Air Traffic Controller without being aware of each other.

4. Pros and Cons of the Mediator Pattern

4.1. Pros of the Mediator Pattern

The mediator pattern undeniably has some distinct advantages that can aid with project complexity:

  • By employing a mediator, the design achieves centralized control over communication logic, streamlining interactions among disparate components. Notably, the pattern allows for seamless modifications to communication logic within the mediator without causing disruptions to individual components, thereby facilitating adaptability and ease of maintenance in dynamic software environments.
  • In architectures with numerous interacting components with complex communication requirements, the mediator pattern shines. This pattern allows components to communicate indirectly through a central mediator, reducing the dependencies between the components.
  • In event-driven systems where components need to react to various events or signals, the mediator pattern provides an effective mechanism for handling and distributing these events. The mediator can act as a central dispatcher, directing events to the appropriate components.

4.2. Cons of the Mediator Pattern

While the mediator pattern offers various advantages in terms of decoupling and centralized communication management, it’s important to acknowledge its potential disadvantages:

  • Introducing a mediator can sometimes lead to an increase in overall system complexity. As the number of mediators and their responsibilities grow, managing and understanding the interactions between components may become more challenging.
  • The mediator becomes a central point for communication. If the mediator fails or experiences issues, the entire system’s communication may be jeopardized, potentially leading to a single point of failure.
  • Maintaining a clean and concise mediator interface becomes crucial as the system evolves. Adding new functionalities or modifying existing ones in the mediator might require adjustments in multiple components, making the system more challenging to maintain.

5. Conclusion

In this article, we discussed the mediator pattern, which is a powerful tool for managing communication in complex systems, promoting maintainability and flexibility. In Kotlin, its implementation is concise and elegant, showcasing the language’s capabilities in expressing design patterns effectively. Therefore, by adopting the mediator pattern, developers can create systems that are modular and easy to extend and maintain.

The complete source code for the example discussed in this article is available over on GitHub.