The mediator pattern
Addy Osmani has a great blog post introducing the mediator pattern in his post Patterns For Large-Scale JavaScript Application Architecture. I’ve recently started working with this pattern, using the mediator.js implementation available via npm. Overall the pattern works well when notifying one module of changes in another; for example when publishing state changes.
However one area where the pattern seems to fall short is when one module wants to request information from another. For example, consider a module that manages a list of jobs. Now consider another module that wants to retrieve a job from that list. An initial implementation using the mediator pattern could look like:
A naive object request implementation
mediator.subscribe('job:loaded', function(job) {
console.log('job loaded');
});
mediator.publish('job:load', jobId);
Some problems with the above implementation include:
-
There is a memory leak with the subscription object returned from the
subscribe
method. -
A race condition exists whereby a second job:load event may be fired and complete before the first one completes; our listener would receive this second job object.
An improved object request implementation
The first issue above can be resolved by appending the jobId
value to the event job:loaded
event. The second issue can be resolved by un-subscribing from the event in the callback function. It turns out this is a common enough pattern that we have the shorthand once
method that accomplished this for us.
mediator.once('job:loaded:'+jobId, function(job) {
console.log('job loaded');
});
mediator.publish('job:load', jobId);
This second implementation indeed corrects the problems of the first implementation, but we still have a source for a potential memory leak, whereby the job:loaded:#
event never fires. We are left with a dangling subscription; there is no error handling in the above implementation.
A possible gotcha can be uncovered if the job load task is synchronous, then the subscription must be created before the requesting event is triggered.
I also don’t like the verbosity of the approach. Triggering the event and registering a callback for the corresponding success event results in a lot of repeated boilerplate code.
Object request with error handling
The error handling issue can be resolved by introducing an error event in addition to the already used success event. To keep things clear I’ll adjust the naming convention of the events to look like:
- job:load
-
An event requesting for the job to be loaded.
- job:load:done
-
An event fired when the job is successfully loaded
- job:load:error
-
An event fired when an error is encountered while loading the job
Secondly we need to set a timeout to ensure any unfulfilled subscriptions do not result in a memory leak.
The result of this is a whole heap of boilerplate code:
var complete = false;
var done = mediator.subscribe('job:load:'+jobId+'done', function(job) {
complete = true;
console.log('job loaded');
});
var error = mediator.subscribe('job:load:'+jobId+'error:', function(job) {
complete = true;
console.error('error loading job');
});
setTimeout(function) {
if (!complete) {
mediator.remove('job:load:'+jobId+'done', done);
mediator.remove('job:load:'+jobId+'error:', error);
console.error('timeout loading job');
}
}, 2000);
mediator.publish('job:load', jobId);
Introducing the mediator.request
method
This can be simplified by encapsulating the above logic in a helper function I call mediator.request
, and adopting the Promise API:
mediator.request('job:load', // the request event
jobId, // the request parameter
'job:load:'+jobId+'done:', // the request success event
'job:load:'+jobId+'error:', // the request error event
2000 // timeout after which an error event is published
).then(function(job) {
console.log('job loaded');
}, function(error) {
console.error('error loading job');
});
Lastly, we can simplify the above invocation by adopting a naming convention for our success and error events using the :<param>:done
and :<param>:error
suffixes respectively (allowing for overrides of course). The resulting API then looks like:
mediator.request('job:load', jobId, [options])
.then(function(job) {
console.log('job loaded');
}, function(error) {
console.error('error loading job');
});
Concerns and conclusion
The above approach for dealing with a request/response communication model between modules using the mediator pattern is loosely based of the HTTP model, where the mediator events map to URLs. The proposed mediator.request
method API is then analogous to the request npm module, and the API could be extended using that module as inspiration.
Finally I’ll mention that I have also considered that it may be an inappropriate use of the mediator pattern when a request/response form of inter-module communication is required. However I feel that with adopting the above API we can maintain the benefits of having loosely-coupled modular architecture provided by the mediator pattern, while addressing the reql-world concern of one module requesting data from another.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Pinterest
Email