The Mediator Pattern

The dictionary refers to a mediator as “a neutral party that assists in negotiations and conflict resolution.”[2] In our world, a mediator is a behavioral design pattern that allows us to expose a unified interface through which the different parts of a system may communicate.

If it appears a system has too many direct relationships between components, it may be time to have a central point of control that components communicate through instead. The Mediator pattern promotes loose coupling by ensuring that instead of components referring to each other explicitly, their interaction is handled through this central point. This can help us decouple systems and improve the potential for component reusability.

A real-world analogy could be a typical airport traffic control system. A tower (mediator) handles which planes can take off and land, because all communications (notifications being listened for or broadcast) are performed from the planes to the control tower, rather than from plane to plane. A centralized controller is key to the success of this system, and that’s really the role that the Mediator plays in software design (Figure 9-5).

Mediator pattern

Figure 9-5. Mediator pattern

In implementation terms, the Mediator pattern is essentially a shared subject in the Observer pattern. This might assume that a direction Publish/Subscribe relationship between objects or modules in such systems is sacrificed in order to maintain a central point of contact.

It may also be considered supplemental—perhaps used for application-level notifications such as a communication between different subsystems that are themselves complex and may desire internal component decoupling through Publish/Subscribe relationships.

Another analogy would be DOM event bubbling and event delegation. If all subscriptions in a system are made against the document rather than individual nodes, the document effectively serves as a mediator. Instead of binding to the events of the individual nodes, a higher level object is given the responsibility of notifying subscribers about interaction events.

Basic Implementation

A simple implementation of the Mediator pattern can be found below, exposing both publish() and subscribe() methods for use:

var mediator = (function(){

    // Storage for topics that can be broadcast or listened to
    var topics = {};

    // Subscribe to a topic, supply a callback to be executed
    // when that topic is broadcast to
    var subscribe = function( topic, fn ){

        if ( !topics[topic] ){ 
          topics[topic] = [];
        }

        topics[topic].push( { context: this, callback: fn } );

        return this;
    };

    // Publish/broadcast an event to the rest of the application
    var publish = function( topic ){

        var args;

        if ( !topics[topic] ){
          return false;
        } 

        args = Array.prototype.slice.call( arguments, 1 );
        for ( var i = 0, l = topics[topic].length; i < l; i++ ) {

            var subscription = topics[topic][i];
            subscription.callback.apply( subscription.context, args );
        }
        return this;
    };

    return {
        publish: publish,
        subscribe: subscribe,
        installTo: function( obj ){
            obj.subscribe = subscribe;
            obj.publish = publish;
        }
    };

}());

Advanced Implementation

For those interested in a more advanced implementation, read on for a walkthrough of my trimmed-down version of Jack Lawson’s excellent Mediator.js. Among other improvements, this version supports topic namespaces, subscriber removal, and a much more robust Publish/Subscribe system for our Mediator. If however, you wish to skip this walkthrough, you can go directly to the next example to continue reading.[3]

To start, let’s implement the notion of a subscriber, which we can consider an instance of a Mediator’s topic registration.

By generating object instances, we can easily update subscribers later without the need to unregister and re-register them. Subscribers can be written as constructors that take a function fn to be called, an options object, and a context.

// Pass in a context to attach our Mediator to. 
// By default this will be the window object
(function( root ){
   
  function guidGenerator() { /*..*/}

  // Our Subscriber constructor
  function Subscriber( fn, options, context ){

    if ( !this instanceof Subscriber ) {

      return new Subscriber( fn, context, options );

    }else{

      // guidGenerator() is a function that generates 
      // GUIDs for instances of our Mediators Subscribers so
      // we can easily reference them later on. We're going
      // to skip its implementation for brevity

      this.id = guidGenerator();
      this.fn = fn;
      this.options = options;
      this.context = context;
      this.topic = null;

    }
  }
})();

Topics in our Mediator hold a list of callbacks and subtopics that are fired when Mediator.Publish is called on our Mediator instance. It also contains methods for manipulating lists of data.

// Let's model the Topic.
// JavaScript lets us use a Function object as a 
// conjunction of a prototype for use with the new 
// object and a constructor function to be invoked.
function Topic( namespace ){

  if ( !this instanceof Topic ) {
    return new Topic( namespace );
  }else{

    this.namespace = namespace || "";
    this._callbacks = [];
    this._topics = [];
    this.stopped = false;

  }
}


// Define the prototype for our topic, including ways to
// add new subscribers or retrieve existing ones.
Topic.prototype = {

  // Add a new subscriber 
  AddSubscriber: function( fn, options, context ){

    var callback = new Subscriber( fn, options, context );

    this._callbacks.push( callback );

    callback.topic = this;

    return callback;
  },
...

Our topic instance is passed along as an argument to the Mediator callback. Further callback propagation can then be called using a handy utility method called StopPropagation():

    StopPropagation: function(){
      this.stopped = true;
    },

We can also make it easy to retrieve existing subscribers when given a GUID identifier:

    GetSubscriber: function( identifier ){

      for(var x = 0, y = this._callbacks.length; x < y; x++ ){
        if( this._callbacks[x].id == identifier || this._callbacks[x].fn == identifier ){
          return this._callbacks[x];
        }
      }

      for( var z in this._topics ){
        if( this._topics.hasOwnProperty( z ) ){
          var sub = this._topics[z].GetSubscriber( identifier );
          if( sub !== undefined ){
            return sub;
          }
        }
      }

    },

Next, in case we need them, we can offer easy ways to add new topics, check for existing topics, or retrieve topics:

    AddTopic: function( topic ){
      this._topics[topic] = new Topic( (this.namespace ? this.namespace + ":" : "") + topic );
    },

    HasTopic: function( topic ){
      return this._topics.hasOwnProperty( topic );
    },

    ReturnTopic: function( topic ){
      return this._topics[topic];
    },

We can explicitly remove subscribers if we feel they are no longer necessary. The following will recursively remove a Subscriber through its subtopics:

    RemoveSubscriber: function( identifier ){

      if( !identifier ){
        this._callbacks = [];

        for( var z in this._topics ){
          if( this._topics.hasOwnProperty(z) ){
            this._topics[z].RemoveSubscriber( identifier );
          }
        }
      }

      for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
        if( this._callbacks[y].fn == identifier || this._callbacks[y].id == identifier ){
          this._callbacks[y].topic = null;
          this._callbacks.splice( y,1 );
          x--; y--;
        }
      }

    },

Next, we include the ability to Publish arbitrary arguments to subscribers recursively through subtopics.

    Publish: function( data ){

      for( var y = 0, x = this._callbacks.length; y < x; y++ ) {

          var callback = this._callbacks[y], l;
            callback.fn.apply( callback.context, data );

        l = this._callbacks.length;

        if( l < x ){
          y--; 
          x = l;
        }
      }

      for( var x in this._topics ){
        if( !this.stopped ){
          if( this._topics.hasOwnProperty( x ) ){
            this._topics[x].Publish( data );
          }
        }
      }

      this.stopped = false;
    }
  };

Here we expose the Mediator instance we will primarily be interacting with. It is through here that events are registered and removed from topics.

  function Mediator() {

    if ( !this instanceof Mediator ) {
      return new Mediator();
    }else{
      this._topics = new Topic( "" );
    }

  };

For more advanced-use cases, we can get our Mediator supporting namespaces for topics such as inbox:messages:new:read.GetTopic, which in the following example, returns topic instances based on a namespace.

  Mediator.prototype = {

    GetTopic: function( namespace ){
      var topic = this._topics,
          namespaceHierarchy = namespace.split( ":" );

      if( namespace === "" ){
        return topic;
      }

      if( namespaceHierarchy.length > 0 ){
        for( var i = 0, j = namespaceHierarchy.length; i < j; i++ ){

          if( !topic.HasTopic( namespaceHierarchy[i]) ){
            topic.AddTopic( namespaceHierarchy[i] );
          }

          topic = topic.ReturnTopic( namespaceHierarchy[i] );
        }
      }

      return topic;
    },

In this section, we define a Mediator.Subscribe method, which accepts a topic namespace, a function fn to be executed, options, and once again a context to call the function into Subscribe. This creates a topic if one doesn’t exist.

    Subscribe: function( topiclName, fn, options, context ){
      var options = options || {},
          context = context || {},
          topic = this.GetTopic( topicName ),
          sub = topic.AddSubscriber( fn, options, context );

      return sub;
    },

Continuing on from this, we can define further utilities for accessing specific subscribers or removing them from topics recursively.

    // Returns a subscriber for a given subscriber id / named function and topic namespace

    GetSubscriber: function( identifier, topic ){
      return this.GetTopic( topic || "" ).GetSubscriber( identifier );
    },

    // Remove a subscriber from a given topic namespace recursively based on
    // a provided subscriber id or named function.

    Remove: function( topicName, identifier ){
      this.GetTopic( topicName ).RemoveSubscriber( identifier );
    },

Our primary Publish method allows us to arbitrarily publish data to a chosen topic namespace.

Topics are called recursively downward. For example, a post to inbox:messages will post to inbox:messages:new and inbox:messages:new:read. It is used as follows:

    Mediator.Publish( "inbox:messages:new", [args] );
    Publish: function( topicName ){
      var args = Array.prototype.slice.call( arguments, 1),
          topic = this.GetTopic( topicName );

      args.push( topic );

      this.GetTopic( topicName ).Publish( args );
    }
  };

Finally we can easily expose our Mediator for attachment to the object passed in to root:

  root.Mediator = Mediator;
  Mediator.Topic = Topic;
  Mediator.Subscriber = Subscriber;

// Remember we can pass anything in here. I've passed in window to
// attach the Mediator to, but we can just as easily attach it to another
// object if desired.
})( window );

Example

Using either of the implementations from above (both the simple option and the more advanced one), we can then put together a simple chat logging system as follows.

Here is the HTML code:

 <h1>Chat</h1>
<form id="chatForm">
    <label for="fromBox">Your Name:</label>
    <input id="fromBox" type="text"/>
    <br />
    <label for="toBox">Send to:</label>
    <input id="toBox" type="text"/>
    <br />
    <label for="chatBox">Message:</label>
    <input id="chatBox" type="text"/>
    <button type="submit">Chat</button>
</form>

<div id="chatResult"></div>

Here is the JavaScript code:

$( "#chatForm" ).on( "submit", function(e) {
    e.preventDefault();

    // Collect the details of the chat from our UI
    var text = $( "#chatBox" ).val(),
        from = $( "#fromBox" ).val(),
        to = $( "#toBox" ).val();

    // Publish data from the chat to the newMessage topic
    mediator.publish( "newMessage" , { message: text, from: from, to: to } );
});

// Append new messages as they come through
function displayChat( data ) {
    var date = new Date(),
        msg = data.from + " said \"" + data.message + "\" to " + data.to;

    $( "#chatResult" )
        .prepend("" + msg + " (" + date.toLocaleTimeString() + ")");
}

// Log messages
function logChat( data ) {
    if ( window.console ) {
        console.log( data );
    }
}



// Subscribe to new chat messages being submitted
// via the mediator
mediator.subscribe( "newMessage", displayChat );
mediator.subscribe( "newMessage", logChat );


// The following will however only work with the more advanced implementation:

function amITalkingToMyself( data ) {
    return data.from === data.to;
}

function iAmClearlyCrazy( data ) {
    $( "#chatResult" ).prepend("" + data.from + " is talking to himself.");
}

 mediator.Subscribe( amITalkingToMyself, iAmClearlyCrazy );

Advantages and Disadvantages

The largest benefit of the Mediator pattern is that it reduces the communication channels needed between objects or components in a system from many to many to just many to one. Adding new publishers and subscribers is relatively easy due to the level of decoupling present.

Perhaps the biggest downside of using the pattern is that it can introduce a single point of failure. Placing a Mediator between modules can cause a performance hit as they are always communicating indirectly. Because of the nature of loose coupling, it’s difficult to establish how a system might react by only looking at the broadcasts.

That said, it’s useful to remind ourselves that decoupled systems have a number of other benefits: if our modules communicated with each other directly, changes to modules (e.g., another module throwing an exception) could easily have a domino effect on the rest of our application. This problem is less of a concern with decoupled systems.

At the end of the day, tight coupling causes all kinds of headaches, and this is just another alternative solution, but one that can work very well if implemented correctly.

Mediator Versus Observer

Developers often wonder what the differences are between the Mediator pattern and the Observer pattern. Admittedly, there is a bit of overlap, but let’s refer back to the GoF for an explanation:

In the Observer pattern, there is no single object that encapsulates a constraint. Instead, the Observer and the Subject must cooperate to maintain the constraint. Communication patterns are determined by the way observers and subjects are interconnected: a single subject usually has many observers, and sometimes the observer of one subject is a subject of another observer.

Both Mediators and Observers promote loose coupling; however, the Mediator pattern achieves this by having objects communicate strictly through the Mediator. The Observer pattern creates observable objects that publish events of interest to objects that are subscribed to them.

Mediator Versus Facade

We will be covering the Facade pattern shortly, but for reference purposes, some developers may also wonder whether there are similarities between the Mediator and Facade patterns. They do both abstract the functionality of existing modules, but there are some subtle differences.

The Mediator pattern centralizes communication between modules where it’s explicitly referenced by these modules. In a sense, this is multidirectional. The Facade pattern, on the other hand, just defines a simpler interface to a module or system but doesn’t add any additional functionality. Other modules in the system aren’t directly aware of the concept of a facade and could be considered unidirectional.



[3] Thanks to Jack for his excellent code comments which assisted with this section.

Get Learning JavaScript Design Patterns now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.