The mediator design pattern

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:

Note

Define an object that encapsulates how a set of other objects interact with each other in order to promote loose coupling and allow us to vary class interactions independently.

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.

Class diagram

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:

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.

Code example

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 :

Code example

As the output shows, our code does exactly what is expected, and it managed to keep the concepts loosely coupled in the application.

What is it good for?

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.

What is it not so good for?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.254.90