Mediator Design Pattern - Definition

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping the objects from referring to each other explicitly, and it lets you vary their interaction independently.

Problem

In object-oriented implementations, the behaviour is split across many objects. There is a natural requirement to have an object depend on others. Quickly, this dependency can grow and have each object depend on all others.

The Mediator design pattern strives to break such complex dependency chain from being formed. It makes all the objects to depend on a single object, the Mediator. The Mediator depends on and knows all the objects. This keeps the individual component objects re-usable and maintainable. It also lets us add new objects into the system easily.

An example - A Smart Home System

Let us say we have a smart home control system application. Consider we have an Air conditioner, a fan and a smart control as part of this application.

If the smart control is on, then it controls and optimizes the air-based components (the AC and the fan). If one of the air components is switched on, the other would be switched off automatically (if the other was also running).

On the other hand if the smart control is off, the two air based components can be running at the same time.

A natural implementation

A straightforward implementation means that the Smart Control, the AC and the Fan classes (the individual components) must know each other i.e., there is a bi-directional dependency between any two sets of classes. In other words, each class knows about the other two. This creates a very tight coupling.

Let us see how the three classes are dependent on the rest of the classes in the system:

  • If smart control is turned on, it has to ensure only one of AC and Fan are running (Smart Control depends on the AC and the Fan)
  • If the Fan is turned on, it has to switch off the AC if smart control is on.
  • Similar to the above, if the AC is turned on, it has to switch off the fan if smart control is on.

Problems with the natural implementation

In software applications, it is never a desired property to have such tight coupling. We must always strive for a design that promotes loose-coupling.

Let us say in the future we add a new component to this application - a light. And there can be new rules, or the existing rule can be changed. If <some_condition>, then do this, that, etc. This will only increase the dependencies among the individual components.

The tight dependency means that the individual components cannot be reused. Again, a bad design sign.

Mediator design pattern to break the dependency

From the business rules, it is evident that there is some relationship or dependency among the three components (Smart Control, the AC and the fan). Mediator design pattern helps us to achieve this without directly coupling the component classes together.

In this, the components do not talk with each other. Each of the component is only aware of the mediator object. They communicate with the other objects through the mediator. The mediator object knows about all the components in the system.

In our example, if the smart control is turned on, and say the AC is turned on, the AC does not switch off the fan. The AC tells the mediator about its state change. The mediator reads the current smart control setting (if smart control is enabled) and turns off the fan (if running).

Thus, a mediator object is responsible for controlling and coordinating the interactions among a group of objects.

Let us look at a sample implementation.

Implementing the mediator design pattern

The components interface

In this example, each of the components has three methods as shown below

public interface HomeComponent {
    void on();
    
    void off();
    
    boolean isOn();
}

The mediator interface

The mediator interface has a single method that the component will call. It passes itself as an argument. With this, the mediator can know which component has made the call. Then, the mediator can access the needed states of the component through some public accessor methods (like isOn() in our example).

public interface SmartHomeMediator {
    void changed(HomeComponent changedHomeComponent);
}

The concrete components

Let us look at the implementation of the component objects.

public class AirConditioner implements HomeComponent {
    private int level;
    private SmartHomeMediator smartHomeMediator;

    @Override
    public void on() {
        System.out.println("Turning on the AC");
        level = 1;
        smartHomeMediator.changed(this);
    }

    @Override
    public void off() {
        System.out.println("Turning off the AC");
        level = 0;
        smartHomeMediator.changed(this);
    }

    @Override
    public boolean isOn() {
        return this.level != 0;
    }

    public void adjustLevel(int level) {
        this.level = level;
    }

    public void setSmartHomeMediator(SmartHomeMediator smartHomeMediator) {
        this.smartHomeMediator = smartHomeMediator;
    }
}
public class Fan implements HomeComponent {
    private int level;
    private SmartHomeMediator smartHomeMediator;

    @Override
    public void on() {
        System.out.println("Turning on the fan");
        level = 1;
        smartHomeMediator.changed(this);
    }

    @Override
    public void off() {
        System.out.println("Turning off the fan");
        level = 0;
        smartHomeMediator.changed(this);
    }

    @Override
    public boolean isOn() {
        return this.level != 0;
    }

    public void adjustLevel(int level) {
        this.level = level;
    }

    public void setSmartHomeMediator(SmartHomeMediator smartHomeMediator) {
        this.smartHomeMediator = smartHomeMediator;
    }
}
public class SmartControl implements HomeComponent {
    private boolean smartControl;
    private SmartHomeMediator smartHomeMediator;

    @Override
    public void on() {
        smartControl = true;
        smartHomeMediator.changed(this);
    }

    @Override
    public void off() {
        smartControl = false;
        smartHomeMediator.changed(this);
    }

    @Override
    public boolean isOn() {
        return smartControl;
    }


    public boolean isSmartControlOn() {
        return smartControl;
    }

    public void setSmartHomeMediator(SmartHomeMediator smartHomeMediator) {
        this.smartHomeMediator = smartHomeMediator;
    }
}

Each of the components has a reference to the mediator object. Whenever their state changes (on/off), they call the changed method of the mediator passing itself as an argument. The adjustLevel method shown is just to demonstrate that the components can be independent and have their own methods.

Mediator implementation

Now comes the most important part - the implementation of the mediator. The changed method implements all the aforementioned state transitions.

public class SmartHomeController implements SmartHomeMediator {
    private final SmartControl smartControl;
    private final AirConditioner airConditioner;
    private final Fan fan;

    public SmartHomeController(SmartControl smartControl, AirConditioner airConditioner, Fan fan) {
        this.smartControl = smartControl;
        this.airConditioner = airConditioner;
        this.fan = fan;
    }

    @Override
    public void changed(HomeComponent changedHomeComponent) {
        if (changedHomeComponent == smartControl && smartControl.isSmartControlOn()) {
            if (airConditioner.isOn() && fan.isOn()) {
                //Turn off fan if both AC and fan are on
                fan.off();
            }
        } else {
            if (smartControl.isSmartControlOn()) {
                if (changedHomeComponent == airConditioner && airConditioner.isOn() && fan.isOn()) {
                    /*
                    Smart control is ON
                    AC has been been turned on now
                    Switch off the fan
                     */
                    fan.off();
                } else if (airConditioner.isOn() && fan.isOn()) { //changedHomeComponent is fan
                    /*
                    Smart control is ON
                    Fan has been been turned on now
                    Switch off the AC
                     */
                    airConditioner.off();
                }
            }
        }
    }
}

The mediator has the reference to all the components. It checks which component has changed.

Case 1: The smart control state has changed

It checks if the smart control has been turned on and if both AC and fan are running. If yes, it turns one of them off (fan in our example).

Case 2: The AC/fan state has changed

It first checks if smart control has been enabled (turned on). If not, it should not do the air control optimisation. If smart control is on and if AC has been turned on, it switches off the fan (and vice versa for the fan case).

Mediator Demo

SmartControl smartControl = new SmartControl();
AirConditioner airConditioner = new AirConditioner();
Fan fan = new Fan();

SmartHomeController smartHomeController = new SmartHomeController(smartControl, airConditioner, fan);
smartControl.setSmartHomeMediator(smartHomeController);
airConditioner.setSmartHomeMediator(smartHomeController);
fan.setSmartHomeMediator(smartHomeController);

smartControl.on(); //Turn on the smart control

airConditioner.on();
fan.on();
System.out.println("AC on ? " + airConditioner.isOn());
System.out.println("Fan on ? " + fan.isOn());

We create each of the components and the mediator and establish the relationship or dependencies among them.

We first turn on the smart control following which we turn on AC and fan. Since the smart control is on, it must switch off the AC. We can see this from the output it produces.

Turning on the AC
Turning on the fan
Turning off the AC
AC on ? false
Fan on ? true

Next, let us turn on the AC. It should turn off the fan

airConditioner.on();
System.out.println("AC on ? " + airConditioner.isOn());
System.out.println("Fan on ? " + fan.isOn());
System.out.println();

Outputs,

Turning on the AC
Turning off the fan
AC on ? true
Fan on ? false

Let us simulate the case where the smart control is off. Now turning on the fan should not do anything to the already running AC.

smartControl.off();// Turn off the smart control
fan.on();
System.out.println("AC on ? " + airConditioner.isOn());
System.out.println("Fan on ? " + fan.isOn());
System.out.println();

Prints,

Turning on the fan
AC on ? true
Fan on ? true

Let us now turn the smart control on back, it must switch off the fan since both AC and fan are currently running.

smartControl.on(); //Turn back on the smart control
System.out.println("AC on ? " + airConditioner.isOn());
System.out.println("Fan on ? " + fan.isOn());

Prints,

Turning off the fan
AC on ? true
Fan on ? false

Structure

MediatorClassDiagram

Participants

Mediator (SmartHomeMediator):

  • It defines an interface for communicating with the Component objects.

ConcreteMediator(SmartHomeController):

  • Knows about all the component objects.
  • Has logic to coordinate among the objects.

Component or Colleague classes (Fan, AirConditioner, SmartControl):

  • Knows about the mediator - does not know about the other Component objects.
  • It communicates with the mediator (when its state is changed).

Pros and Cons of the Mediator Design pattern

Advantages

  1. It decouples the component classes. A component only depends on the mediator. It makes a many-to-many interaction a one-to-many interaction.
  2. It makes the components reusable.
  3. Since a mediator has a clear interface, we can create and use a new mediator in the future.

Disadvantages

As you would have noticed, the mediator implementation is heavy. It would seem to know a lot of things. If not careful, it can become a God object.

One possible way to implement the mediator is by using an Observer design pattern. The Component acts as a Subject and sends notification to the Mediator whenever their state changes. The mediator responds to the notification by calling the methods of other components.

Facade Design Pattern is similar. But a facade abstracts a subsystem of objects to provide a more simplified interface. The subsystem is unaware of the facade. The objects within the subsystem can communicate with each other. On the other hand, a mediator centralizes the communication so that the components do not communicate with each other and instead knows about the mediator.

Conclusion

We learnt about the Mediator Design Pattern in this post. A Mediator centralizes the communication and prevents a set of components from becoming highly coupled. All the components that want to communicate with the others do so through the mediator. A component calls a method on the mediator passing its reference. The mediator knows about all the components and thus can identify which component has sent the message. The mediator reads the state of the component and decides what to do. It calls the other components based on that. With this, the source component does not know what the receiving component(s) will be. Similarly, the receiving component does not know what the source is. Thus, the components are decoupled.

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
  2. Refactoring Guru’s Mediator Pattern
  3. Wikipedia – Mediator Pattern
  4. Mediator Design Pattern – Howtodoinjava