Flutter Design Patterns: 22 — Mediator
An overview of the Mediator design pattern and its implementation in Dart and Flutter
Previously in the series, I analysed a behavioural design pattern that separates algorithms from the objects they operate on — Visitor. This time I would like to represent one another behavioural design pattern that lets you reduce dependencies between a set of interacting objects by decoupling the interaction logic from the objects and moving it to a dedicated controller — it is the Mediator.
Update 2022–09–15: I moved this blog to my personal website. To see the most updated version of this article and code examples, check kazlauskas.dev.
Table of Contents
- What is the Mediator design pattern?
- Other articles in this series
- Your contribution
What is the Mediator design pattern?
Mediator, also known as Intermediary or Controller, is a behavioural design pattern, which intention in the GoF book is described like this:
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
TL;DR: the main target of the Mediator design pattern is to move from the communicating object dependencies chaos provided on the left to the decoupled one provided on the right:
In the Mediator design pattern context, communicating objects are called colleagues while the object that controls and coordinates the interaction is called *drums rolls* the mediator.
The mediator is like a telephone exchange that keeps references to interacting objects and maintains all the required logic to “connect” colleague A with colleague B. As a result, colleague objects have no explicit knowledge of each other, they only refer to their mediator — in the OOP world, we could say that objects are loosely coupled. This allows reusing individual colleague objects independently since they have fewer dependencies on the other objects.
Another upside of the Mediator design pattern is that it simplifies and abstracts the way how objects interact. First of all, the mediator replaces many-to-many (N:M) relationships with one-to-many (1:N) interactions between the mediator and its colleagues. In general, 1:N relationships are just easier to understand and maintain. Besides, the mediator object abstracts the interaction logic — colleagues should be aware only of the communication act but not of any details on how it is implemented. This abstraction enables adding new mediators without changing the actual components. Also, having the whole communication logic in a single place helps a lot when you need to adjust or maintain it.
Let’s just jump right in by analysing the Mediator design pattern and its implementation in more detail!
The general structure of the Mediator design pattern looks like this:
- Mediator — defines an interface for communicating with components;
- ConcreteMediator — encapsulates relations between components by containing references to them;
- (Optional) Abstract Component or Component Interface — similar communicating components could implement the same interface or extend the same base class. In this case, ConcreteMediator could store a list of components extending/implementing this class instead of keeping multiple references as separate properties;
- Concrete component or Colleague — contains a reference to a mediator. Each colleague communicates with its mediator whenever it would have otherwise communicated with another colleague (component).
The Mediator design pattern should be used when instead of having tightly coupled classes you want to have loose-coupled ones, because:
a) You want to reuse the component elsewhere. When a component is too dependent on other classes, it’s hard to reuse it as a stand-alone object.
b) You want to make changes in some of the classes, but they affect other dependencies. By using the Mediator design pattern, the relationship logic between objects is extracted to a separate class, hence the changes could be implemented without directly affecting the rest of the components.
Also, you should consider using the Mediator design pattern when there is a need to add communicating objects at run-time. Since the mediator class takes care of the communication logic and all the dependencies between objects, it’s possible to add or remove those dependencies later from the code just like adding a new user to the chat room.
However, by moving all the communication logic to a dedicated class there is a risk to end up having a God Object. To avoid this, make sure that the mediator class is only responsible for the communication part. If you notice any other calculations, data manipulations or extraneous operations (Eminem would be proud of this line, I think) they should be extracted to a dedicated class.
We will use the Mediator design pattern to implement a notification hub for the engineering team.
Let’s say that we want a solution to send notifications to other team members. Inside the team, there are 3 main roles: Admin a.k.a. God, Developer and tester (QA engineer). There are times when the admin wants to send notifications to the whole team or members of a specific role. Also, any other team member should be able to send a quick note to the whole team, too.
If you think of this problem, you could quickly notice a many-to-many relationship between team members — every engineer should be aware of the others just to send the notification. For this reason, we will implement a centralised way to send notifications — a notification hub. You could think of it as a chat room — every team member joins the hub and later they use it to send notifications by simply calling a send method. Then, the hub distributes the message to the others — to all of them or by specific role.
By using this solution, team members should not be aware of the others, they are completely decoupled. Also, in the case of a new team member, it is enough to add him/her to the notification hub and you could be sure that all the notifications would be delivered.
Sounds too good to be true? Watch and learn!
The class diagram below shows the implementation of the Mediator design pattern:
TeamMember is an abstract class that is used as a base class for all the specific team member classes. The class contains name, lastNotification and notificationHub properties, and provides several methods:
- receive() — receives the notification from the notification hub;
- send() — sends a notification;
- sendTo<T>() — sends a notification to specific team members.
Admin, Developer and Tester are concrete team member classes that extend the abstract class TeamMember as well as override the default toString() method.
NotificationHub is an abstract class that is used as a base class for all the specific notification hubs and defines several abstract methods:
- getTeamMembers() — returns a list of team members of the hub;
- register() — registers a team member to the hub;
- send() — sends a notification to registered team members;
- sendTo<T>() — sends a notification to specific registered team members.
TeamNotificationHub is a concrete notification hub that extends the abstract class NotificationHub and implements its abstract methods. Also, this class contain a list of registered team members — teamMembers.
MediatorExample initialises and contains a notification hub property to send and receive notifications, and register team members to the hub.
An abstract class implementing base methods for all the specific team member classes. Method receive() sets the lastNotification value, send() and sendTo<T>() methods send notification by using the corresponding notificationHub methods.
Concrete team member classes
All of the specific team member classes extend the TeamMember and override the default toString() method.
- Admin — a team member class representing the admin role.
- Developer — a team member class representing the developer role.
- Tester — a team member class representing the tester (QA) role.
An abstract class that defines abstract methods to be implemented by specific notification hub classes. Method getTeamMembers() returns a list of registered team members to the hub, and register() registers a new member to the hub. Method send() sends the notification to all the registered team members to the hub (excluding sender) while sendTo<T>() sends the notification to team members of a specific type (excluding sender).
A specific notification hub implementing abstract NotificationHub methods. The class also contains private teamMembers property — a list of registered team members to the hub.
First of all, a markdown file is prepared and provided as a pattern’s description:
The MediatorExample widget initialises the TeamNotificationHub and later uses it to send notifications between team members.
Specific team members do not contain any reference to the others, they are completely decoupled. For communication, the notification hub is used that handles all the necessary logic to send and receive notifications from the team.
As you can see in the example, you could send notifications from different team members, and add new members later to the hub so they will be notified, too.
All of the code changes for the Mediator design pattern and its example implementation could be found here.
Other articles in this series
- 0 — Introduction
- 1 — Singleton
- 2 — Adapter
- 3 — Template Method
- 4 — Composite
- 5 — Strategy
- 6 — State
- 7 — Facade
- 8 — Interpreter
- 9 — Iterator
- 10 — Factory Method
- 11 — Abstract Factory
- 12 — Command
- 13 — Memento
- 14 — Prototype
- 15 — Proxy
- 16 — Decorator
- 17 — Bridge
- 18 — Builder
- 19 — Flyweight
- 20 — Chain of Responsibility
- 21 — Visitor
- 23 — Observer
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues on social media.
➕ Follow me on Medium.
⭐ Star the Github repository.