Real-world software projects usually contain a large number of different classes. This helps to distribute complexity and logic so that each class does one specific thing, which is simple, rather than many complex tasks. This, however, requires classes to communicate with each other in some way in order to realize some specific functionality, but then keeping the loose coupling principle in place could become a challenge. The purpose of the mediator design pattern is to:
The mediator design pattern defines a specific object called mediator that enables other ones to communicate with each other instead of doing this directly. This reduces dependencies between them, which makes a program easy to change and maintain in the future as well as have it properly tested.
Let's imagine that we are building a system for a school where each student can take multiple classes and each class is taken by multiple students. We might want to have a functionality that notifies all the students of a specific class that it is canceled, or we might want to easily add or remove users from classes. We can impulsively start writing our code and have a list of classes as a part of the student class and a list of students in the group class. This way, however, our objects will become interconnected and not really reusable. This is a good use case for the mediator pattern.
Let's take a look at our class diagram:
As you can see from the preceding diagram, the school is the mediator and it contains information about users to groups and groups to users. It manages the interaction between these entities and allows us to make our Student
and Group
classes reusable and independent from each other.
We've given an example with students and classes; however, this could be easily applied to any many-to-many relationships—permission groups in software, taxi systems, air traffic control systems, and many more.
Now that we have presented our class diagram, let's take a look at the source code for the example. First of all, let's see the model classes we have:
trait Notifiable { def notify(message: String) } case class Student(name: String, age: Int) extends Notifiable { override def notify(message: String): Unit = { System.out.println(s"Student $name was notified with message: '$message'.") } } case class Group(name: String)
The Notifiable
trait in the preceding code is not needed in the current example; however, for example if we add teachers, then it would be useful in the cases where we want to send notifications to everyone in the same group. The classes in the previous code can have their own independent functionality.
Our Mediator
trait has the following definition:
trait Mediator { def addStudentToGroup(student: Student, group: Group) def isStudentInGroup(student: Student, group: Group): Boolean def removeStudentFromGroup(student: Student, group: Group) def getStudentsInGroup(group: Group): List[Student] def getGroupsForStudent(student: Student): List[Group] def notifyStudentsInGroup(group: Group, message: String) }
As you can see , the preceding code defines methods that allow interactions between students and groups. The implementation of these methods is as follows:
class School extends Mediator { val studentsToGroups: Map[Student, Set[Group]] = Map() val groupsToStudents: Map[Group, Set[Student]] = Map() override def addStudentToGroup(student: Student, group: Group): Unit = { studentsToGroups.getOrElseUpdate(student, Set()) += group groupsToStudents.getOrElseUpdate(group, Set()) += student } override def isStudentInGroup(student: Student, group: Group): Boolean = groupsToStudents.getOrElse(group, Set()).contains(student) && studentsToGroups.getOrElse(student, Set()).contains(group) override def getStudentsInGroup(group: Group): List[Student] = groupsToStudents.getOrElse(group, Set()).toList override def getGroupsForStudent(student: Student): List[Group] = studentsToGroups.getOrElse(student, Set()).toList override def notifyStudentsInGroup(group: Group, message: String): Unit = { groupsToStudents.getOrElse(group, Set()).foreach(_.notify(message)) } override def removeStudentFromGroup(student: Student, group: Group): Unit = { studentsToGroups.getOrElse(student, Set()) -= group groupsToStudents.getOrElse(group, Set()) -= student } }
The School
is the actual mediator that our application will be using. As you can see, it does exactly what the mediator design pattern is supposed to do— keeps the objects from directly referring to each other and internally defines their interactions. An application that uses our School
class is shown in the following code:
object SchoolExample { def main(args: Array[String]): Unit = { val school = new School // create students val student1 = Student("Ivan", 26) val student2 = Student("Maria", 26) val student3 = Student("John", 25) // create groups val group1 = Group("Scala design patterns") val group2 = Group("Databases") val group3 = Group("Cloud computing") school.addStudentToGroup(student1, group1) school.addStudentToGroup(student1, group2) school.addStudentToGroup(student1, group3) school.addStudentToGroup(student2, group1) school.addStudentToGroup(student2, group3) school.addStudentToGroup(student3, group1) school.addStudentToGroup(student3, group2) // notify school.notifyStudentsInGroup(group1, "Design patterns in Scala are amazing!") // see groups System.out.println(s"$student3 is in groups: ${school.getGroupsForStudent(student3)}") // remove from group school.removeStudentFromGroup(student3, group2) System.out.println(s"$student3 is in groups: ${school.getGroupsForStudent(student3)}") // see students in group System.out.println(s"Students in $group1 are ${school.getStudentsInGroup(group1)}") } }
The preceding example application is really simple—it creates objects of the Student
and Group
types and uses the mediator object to wire them up and make it possible for them to interact. The output of the example is as shown :
As the output shows, our code does exactly what is expected, and it managed to keep the concepts loosely coupled in the application.
The mediator design pattern is good for keeping coupling between classes loose in an application. It helps to achieve simplicity and maintainability while still allowing us to model complex interactions between objects in our applications.
A possible pitfall when using the mediator design pattern is to put a lot of different interaction functionalities in one class. Mediators tend to become more complex with time, and it will become hard to change or understand what our application can do at all. Moreover, if we actually have many more classes that have to interact with each other, it will imminently affect the mediator as well.
3.143.254.90