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
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
- It decouples the component classes. A component only depends on the mediator. It makes a many-to-many interaction a one-to-many interaction.
- It makes the components reusable.
- 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.
Related Design Patterns
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
- Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
- Refactoring Guru’s Mediator Pattern
- Wikipedia – Mediator Pattern
- Mediator Design Pattern – Howtodoinjava