Handle complex objects in JavaScript with Mediator pattern

Working as a consultant in the software industry is a real challenge in many ways, and as a self-employed consultant it's even more. There are different roles I need to handle and to not suffer my core knowledge - develop software - I try to think about software development the same way I thought about Tennis when I was younger;

Most people have heard about, "to master something, you need to practice that thing 10 000 times."

It's a phrase very common in the sports world. I think it was Willi Railo, the Norwegian sport's psychologist, that first wrote about it in his book, 'Best When it Counts'.

As a self-employed consultant, I have no goal to be a top sales person, I don't want to master marketing either, however, I want to always stay on top when it comes to software development and want to improve my communication skills.

That's wy I'm writing articles about my training, and I spend a lot of time training and love it.

In this article I choose to write about a quiet complex pattern that I can never train enough on - the mediator pattern.

The mediator pattern

The Mediator pattern provides central authority over a group of objects by encapsulating how these objects interact. This model is useful for scenarios where there is a need to manage complex conditions in which every object is aware of any state change in any other object in the group.

The Mediator patterns are useful in the development of complex forms. Take for example a page in which you enter options to make a flight reservation. A simple Mediator rule would be: you must enter a valid departure date, a valid return date, the return date must be after the departure date, a valid departure airport, a valid arrival airport, a valid number of travelers, and only then the Search button can be activated.

Another example of Mediator is that of a chat room controlling messages sent and received between participants.

Chat room example

In the example code we have four participants that are joining a chat session by registering with a chat room (the Mediator). Each participant is represented by a Participant object. Participants send messages to each other and the ChatRoom handles the routing logic.

The chat room;

  • defines an interface for communicating with Participant objects,
  • maintain references to Participant objects,
  • manages central control over operations for sending and receiving messages.

The participants;

  • objects that are being mediated by the Mediator,
  • each instance maintains a reference to the Mediator,
  • in this demo, stores received messages to the mediator logger store to keep history of all messages sent.

First we set up the code for the participants.

/*
 * These symbols are used to represent own properties that should not be part of
 * the public interface. We can however choose to do so by getters and setters.
 * You could also use ES2019 private fields, but those
 * are not yet widely available as of the time of my writing.
 */
const participantName = Symbol('participantName')
const chatRoom = Symbol('chatRoom')

/**
 * Participants class - send and receive messages from other participants
 */
class Participant {
  constructor(name) {
    /**
     * Name of the participant
     */
    this[participantName] = name
    /**
     * Reference to the chat room - is set after registration to a chat room
     */
    this[chatRoom] = null
  }

  /**
   * Getter for the participantName property
   * @returns {string}
   */
  get participantName() {
    return this[participantName]
  }

  /**
   * Setter for the chat room property
   * @param {ChatRoom} chatRoom - chat room instance
   */
  set chatRoom(chatRoom) {
    this[chatRoom] = chatRoom
  }

  /**
   * Send a message via the chatRoom (mediator)
   * @param {string} message - message
   * @param {Participant} receiver - participant object to receive the message
   */
  send(message, receiver) {
    this[chatRoom].send(message, this, receiver)
  }

  /**
   * Receive message from the chatRoom - is delegated to the chatRoom object
   * @param {string} message - message
   * @param {Participant} sender - participant object that sent the message
   * @remarks Store the received message in the chat room logger for this demo.
   */
  receive(message, sender) {
    this[chatRoom].logger = sender.participantName + ' to ' + this.participantName + ': ' + message
  }
}

The participant object is simple and have an interface containing a property setter to set the chat room, a property getter to get the name of the participant and public methods to send and receive messages. The receive method is delegated to the chat room, and the send method invokes the send method of the chat room.

Now, lets create the code for the chat room.

/*
 * These symbols are used to represent own properties that should not be part of
 * the public interface. We can however choose to do so by getters and setters.
 * You could also use ES2019 private fields, but those
 * are not yet widely available as of the time of my writing.
 */
const room = Symbol('room')
const participants = Symbol('participants')
const logger = Symbol('logger')

/**
 * The mediator
 */
class ChatRoom {
  constructor(name) {
    /**
     * Name of the chat room
     */
    this[room] = name
    /**
     * Store for participants
     */
    this[participants] = {}
    /**
     * Logger store for all messages in this chat room
     */
    this[logger] = ''
  }

  /**
   * Setter for the message logger
   * @param {string} message - message to store
   */
  set logger(message) {
    this[logger] = this[logger] + message + '\n'
  }

  /**
   * Get all logged messages
   * @returns {string}
   */
  getLog() {
    return 'Log from chat room: ' + this[room] + '\n' +  this[logger]
  }

  /**
   * Register a participant
   * @param {Participant} participant - participant to register
   */
  register(participant) {
    // Update reference store
    this[participants][participant.participantName] = participant

    // Bind the participant with the this chat room
    participant[chatRoom] = this
  }

  /**
   * Rout messages
   * @param {string} message - message to be routed
   * @param {Participant} sender - participant object that sends a message
   * @param {Participant} receiver - participant object to receive the message
   */
  send(message, sender, receiver) {
    // If no receiver specified - broadcast to all but sender
    if (!receiver) {
      for (let participant in this[participants]) {
        // Filter out sender
        if (participant !== sender.participantName) {
          // Call the participant's receive method
          this[participants][participant].receive(message, sender)
        }
      }
    } else {
      // Call the participant's receive method
      receiver.receive(message, sender)
    }
  }
}

The mediator, the chat room class, is a little more complex and have a property setter to update the internal logger store with a history of all messages sent and public methods, getLog() to get all messages from the logger store, register() to register a participant for the chat room, and send() method that is responsible for the routing logic between all participants.

Let's test this, here come a small example og how to use it.

// create participants
let yoko = new Participant('Yoko')
let john = new Participant('John')
let paul = new Participant('Paul')
let ringo = new Participant('Ringo')

// Create a chat room
let chatRoom = new ChatRoom('Beatles')

// Register participant to the chat room  
chatRoom.register(yoko)
chatRoom.register(john)
chatRoom.register(paul)
chatRoom.register(ringo)

// Send messages
yoko.send('All you need is love.')
yoko.send('I love you John.')
john.send('Hey, no need to broadcast.', yoko)
paul.send('Ha, I heard that!')
ringo.send('Paul, what do you think?', paul)

// Get the messages sent from the logger store
let log = chatRoom.getLog()
console.log(log)
// Outputs
//  'Log from chat room: Beatles' + '\n' +
//  'Yoko to John: All you need is love.' + '\n' +
//  'Yoko to Paul: All you need is love.' + '\n' +
//  'Yoko to Ringo: All you need is love.' + '\n' +
//  'Yoko to John: I love you John.' + '\n' +
//  'Yoko to Paul: I love you John.' + '\n' +
//  'Yoko to Ringo: I love you John.' + '\n' +
//  'John to Yoko: Hey, no need to broadcast.' + '\n' +
//  'Paul to Yoko: Ha, I heard that!' + '\n' +
//  'Paul to John: Ha, I heard that!' + '\n' +
//  'Paul to Ringo: Ha, I heard that!' + '\n' +
//  'Ringo to Paul: Paul, what do you think?' + '\n'

That's it for now.


Published: 2020-01-25
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB