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).
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.
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
;
}
};
}());
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
);
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 );
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.
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.
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.
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.