Before I left my previous company, I decided to introduce the mediator pattern in order to encapsulate the communication between our data models and the WebSocket. Although the implementation was a bit simple, I think it has a lot of power and future possibilities, making the code cleaner and unit testing live updates easier.
To read more about JavaScript design patterns there is an amazing book written by Addy Osmani and a follow up, where you can understand better the mediator pattern.
A basic mediator provides methods to subscribe, publish and unsubscribe from a channel, keeping track of the channels and the methods that have to be executed on the subscriptors when new content is published to a channel.
angular.module('webApp') .service('Mediator', function Mediator() { var channels = {}; // Associative object. this.publish = function(channel, data) { if (! channels[channel]) { return; } var subscribers = channels[channel].slice(); angular.forEach(subscribers, function(subscriber) { subscriber.callback(data); }); }; this.subscribe = function(channel, id, cb) { if (! channels[channel]) { channels[channel] = []; } return channels[channel].push({ 'callback': cb, 'id': id }); }; this.unsubscribe = function (channel, id) { if (! channels[channel]) { return false; } for(var i = 0, len = channels[channel].length; i < len; i++) { if (channels[channel][i].id === id) { var removed = channels[channel].splice(i, 1); return (removed.length > 0); } } return false; }; });
Our data models then subscribe to a determined channel so they can get updated.
angular.module('webApp') .factory('Match', function (Mediator) { function Match(data) { angular.extend(this, data); Mediator.subscribe('match:update', this.id, angular.bind(this, this.update)); } Match.prototype.update = function(updatedMatch) { // Update model with new data. }; Match.prototype.close = function() { Mediator.unsubscribe('match:update', this.id); }; return Match; });
And the socket publishes to the corresponding channel every time that an update is received.
angular.module('webApp') .factory('socket', function (Mediator) { var socket = io(); // Socket.IO exposes a global io variable. socket.on('match:update', function (data) { Mediator.publish('match:update', data); }); return socket; });
This way we could have several sources of information to update our models, or we could get rid easily of Socket.IO and substitute it with other library, without modifying the code of our data model objects.
Disclaimer: my goal with this implementation was to avoid injecting the vilified $scope
to our objects and decoupling the socket events, what worked well for us making unit testing easier.
My implementation of the pattern is similar to Mediator.js that can be easily confused with the publish-subscribe pattern. Therefore a truly implementation of the mediator pattern in AngularJS could look like this angular-mediator.
Reblogged this on Dinesh Ram Kali..
This looks like Pub/Sub to me, not Mediator. Could you explain how this represents the Mediator pattern?
Hi Nick!
My idea of the mediator was of an object that encapsulates the communication between other objects and contains some logic to accomplish it. But I trimmed down most of the logic for the example so I think you are right and this looks more like the publish–subscribe pattern.
I guess the problem is that both patterns are often interchanged and the difference is subtle. For example I’ve found this Mediator.js which looks really similar to my implementation of the pattern.
So I definitely need to think about it and update this post.
Cheers, Pablo.
Hi Pablo,
these functions are great. However, I found one “bug”. If you unsubscribe from a channel inside a publish-event, the direct following listener will never get called. To solve this, it’s necessary to clone the channel-array before calling the listeners. So this is the working solution:
this.publish = function(channel, data){
if (!channels[channel]){
return;
}
var subscribers = channels[channel].slice();
for(var i = 0, length = subscribers.length; i < length; i++){
subscribers[i].callback(data);
}
};
Hi Julian!
This is interesting. How many subscribers did you have? Because I have tried to reproduce it unsuccessfully. If I understand correctly it happens when a subscriber unsubscribes at the same time a publish is happening.
Cheers, Pablo.
Hi Pablo,
You are right. I had only two listeners, both listened to the same channel. But the first listener removed his subscription when he was called in the publish-event. Then in the background the second listener got the array-index of the removed (first) listener (0), because the publish-method works with the live subscriber-array with only a reference to it. That’s why the index 1 did’nt exist in the next angular.forEach-call and the second listener never got called. I hope it’s clear now?
Aha, I see it now. I never thought of that user case so I’m updating the post right away. Thanks Julian!
What is the advantage of this pattern towards $on and $emit?
Hi Julian! My idea was to avoid the use of $scope at all. Cheers, Pablo.