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.
To see all the design patterns in action, check the Flutter Design Patterns application.
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!
Analysis​
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).
Applicability​
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.
Implementation​
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!
Class diagram​
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.
TeamMember​
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.
abstract class TeamMember {
final String name;
NotificationHub? notificationHub;
String? lastNotification;
TeamMember({
required this.name,
});
void receive(String from, String message) {
lastNotification = '$from: "$message"';
}
void send(String message) {
notificationHub?.send(this, message);
}
void sendTo<T extends TeamMember>(String message) {
notificationHub?.sendTo<T>(this, message);
}
}
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.
class Admin extends TeamMember {
Admin({
required super.name,
});
String toString() {
return "$name (Admin)";
}
}
Developer
 - a team member class representing the developer role.
class Developer extends TeamMember {
Developer({
required super.name,
});
String toString() {
return "$name (Developer)";
}
}
Tester
 - a team member class representing the tester (QA) role.
class Tester extends TeamMember {
Tester({
required super.name,
});
String toString() {
return "$name (QA)";
}
}
NotificationHub​
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).
abstract class NotificationHub {
List<TeamMember> getTeamMembers();
void register(TeamMember member);
void send(TeamMember sender, String message);
void sendTo<T extends TeamMember>(TeamMember sender, String message);
}
TeamNotificationHub​
A specific notification hub implementing abstract NotificationHub
methods. The class also contains private teamMembers
property - a list of registered team members to the hub.
class TeamNotificationHub extends NotificationHub {
final _teamMembers = <TeamMember>[];
TeamNotificationHub({
List<TeamMember>? members,
}) {
members?.forEach(register);
}
List<TeamMember> getTeamMembers() => _teamMembers;
void register(TeamMember member) {
member.notificationHub = this;
_teamMembers.add(member);
}
void send(TeamMember sender, String message) {
final filteredMembers = _teamMembers.where((m) => m != sender);
for (final member in filteredMembers) {
member.receive(sender.toString(), message);
}
}
void sendTo<T extends TeamMember>(TeamMember sender, String message) {
final filteredMembers =
_teamMembers.where((m) => m != sender).whereType<T>();
for (final member in filteredMembers) {
member.receive(sender.toString(), message);
}
}
}
Example​
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.
class MediatorExample extends StatefulWidget {
const MediatorExample();
_MediatorExampleState createState() => _MediatorExampleState();
}
class _MediatorExampleState extends State<MediatorExample> {
late final NotificationHub _notificationHub;
final _admin = Admin(name: 'God');
void initState() {
super.initState();
final _members = [
_admin,
Developer(name: 'Sea Sharp'),
Developer(name: 'Jan Assembler'),
Developer(name: 'Dove Dart'),
Tester(name: 'Cori Debugger'),
Tester(name: 'Tania Mocha'),
];
_notificationHub = TeamNotificationHub(members: _members);
}
void _sendToAll() {
setState(() {
_admin.send('Hello');
});
}
void _sendToQa() {
setState(() {
_admin.sendTo<Tester>('BUG!');
});
}
void _sendToDevelopers() {
setState(() {
_admin.sendTo<Developer>('Hello, World!');
});
}
void _addTeamMember() {
final name = '${faker.person.firstName()} ${faker.person.lastName()}';
final teamMember = faker.randomGenerator.boolean()
? Tester(name: name)
: Developer(name: name);
setState(() {
_notificationHub.register(teamMember);
});
}
void _sendFromMember(TeamMember member) {
setState(() {
member.send('Hello from ${member.name}');
});
}
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
PlatformButton(
text: "Admin: Send 'Hello' to all",
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _sendToAll,
),
PlatformButton(
text: "Admin: Send 'BUG!' to QA",
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _sendToQa,
),
PlatformButton(
text: "Admin: Send 'Hello, World!' to Developers",
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _sendToDevelopers,
),
const Divider(),
PlatformButton(
text: "Add team member",
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _addTeamMember,
),
const SizedBox(height: LayoutConstants.spaceL),
NotificationList(
members: _notificationHub.getTeamMembers(),
onTap: _sendFromMember,
),
],
),
),
);
}
}
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.
To see the pattern in action, check the interactive Mediator example.