http://dev.housetrip.com/2014/09/15/decoupling-javascript-apps-using-pub-sub-pattern/
https://gist.github.com/fatihacet/1290216
The problem
Let me use one of the most common features in modern web applications to introduce the problem: sending notification emails.
Let’s say we’re building an e-commerce site and we’d like to send a notification email to the customer when they make a purchase. A simple solution, and probably the most common one, could be something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var Order = function(params) { this.params = params; }; Order.prototype = { save: function() { // save order console.log("Order saved"); this.sendEmail(); }, sendEmail: function() { var mailer = new Mailer(); mailer.sendPurchaseEmail(this.params); } }; var Mailer = function() {}; Mailer.prototype = { sendPurchaseEmail: function(params) { console.log("Email sent to " + params.userEmail); } }; > var order = new Order({ userEmail: 'john@gmail.com' }) > order.save(); > "Order saved" > "Email sent to john@gmail.com" |
Notice how in sendEmail definition, it creates an instance of Mailer and calls function “sendPurchaseEmail”.
These two objects now have tight coupling because if you were to change sendPurchaseEmail to another name, you’ll have to do the same for sendEmail in Order. In other words, if you were to change the # of parameters, or the interface of the sendPurchaseEmail function, you’ll have to repeat those steps for sendEmail in Order.
Usually you know two components are coupled when a change to one requires a change in the other one, and that’s the case. If we want to change the name of the sendPurchaseEmail method or their params, we’ll have to change Order implementation as well. In a large application this bad practice means having a tightly coupled application where a small change can easily end up in a waterfall of changes.
What we want is Loose Coupling. Loose coupling is when you change the interface, but do not have to worry about changing it in all the other places that you have used it.
What is the Publish Subscribe Pattern?
In software architecture, publish–subscribe is a messaging pattern where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers. Instead, published messages are characterized into classes, without knowledge of what, if any, subscribers there may be. Similarly, subscribers express interest in one or more classes, and only receive messages that are of interest, without knowledge of what, if any, publishers there are.
This pattern uses an event system that sits between the objects wishing to receive notifications (subscribers) and the objects firing the events (the publishers). This event system allows code to define application specific events which can pass arguments containing values needed by the subscriber. The goal is to avoid dependencies between the subscriber and the publisher.
Implementation
First, we declare a global literal object. This object takes will have the functionalities of publishing and subscribing.
1 2 |
// global object of empty literal object var pubsub = {}; |
It will be passed into an IIFE so that it has private block scope for its data structures.
1 2 3 4 5 6 7 |
(function(q){ // has block scope. // all private data structures and properties will not be accessible by others // public properties exposes what it allows }(pubsub)); // we pass parameter pubsub into the IIFE |
then we create the data structures and variables needed for the implementation.
Say there’s a couple of topics: Badminton, Boxing, and Swimming.
First, we use the concept of key: value in order to keep track of them.
The key is the activity name “Badminton”, “Boxing”, “Swimming”.
We have a topics literal object that will store these keys.
TOPICS:
{
“Badminton” : [subscriber1, subscriber2, subscriber3….subscriber n]
“Boxing” : [{token: abcdefg, func : callback}… {token: 38fje8, func : callback}],
“Swimming” : [{token: a1b2c3d4, func : callback}… {token: 0fjdue7, func : callback}],
…
etc
}
Then for each key, we will have a literal object as the value.
That literal object has 2 properties:
– token
– func
token is simply an uid
func is the callback provided by the subscribers. Essentially, whenever something happens
and we want to publish something, we call this func, then pass in some data if needed.
“Badminton” : {token: a4456iol, func : callback}
Hence for each topic, we have an array of objects. Each of these objects have token and func properties. These objects represent
external objects that has subscribed to us and want to receive notifications when something happens.
Subscribe
Therefore, we create a public function property called subscribe. Its first parameter is topic, which is the topic that the external object
wants to subscribe to. For example, badminton, swimming…etc. The func, is the callback that we can use to notify that external object.
1 2 3 4 |
q.subscribe = function(topic, func) { ... ... } |
We must check to see if the topic exist. If there’s only data for say Badminton, and we subscribe to “swimming”, our pubsub system
will create an key/value entry for swimming and then create an empty array. This readies future external objects that wants to
subscribe to the topic “swimming”.
Then, we simply use a increment to simulate id generator. It is assigned to the subscriber. That way, each subscriber has an unique id to identify itself.
1 2 3 4 5 6 7 8 9 10 |
q.subscribe = function(topic, func) { if (!topics[topic]) { topics[topic] = []; } var token = (++subUid).toString(); ... ... } |
Finally, we create an object to store the token and the callback for the subscriber. We store this object into the array that corresponds to the topic that the subscriber wanted.
When we publish, we call execute this callback so that its owner will receive it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
q.subscribe = function(topic, func) { if (!topics[topic]) { topics[topic] = []; } var token = (++subUid).toString(); // INSERT A SUBSCRIBER topics[topic].push({ token: token, // identifier for the subscriber func: func // callback of the subscriber }); // return the key used for this object that's stored in in topics return token; }; |
Publish
So we have all these subscribers stored for different topics and now we’re ready to publish!
We create a public property for the pubsub. The first parameter topic represents the topic we want to publish for.
args is any published data you want to pass to the objects stored in arrays of different topics (subscribers).
1 2 3 4 |
q.publish = function(topic, args) { }; |
First we check to see if there are any subscribers for the topic at hand. If say we have 3 subscribers for “Badminton”, and we try to publish something for “swimming”, we just spit out an error message because no such topic exist.
{
“Badminton” : [subscriber1, subscriber2, subscriber3]
}
1 2 3 4 5 6 7 8 9 10 |
q.publish = function(topic, args) { if (!topics[topic]) { console.log("Error, object with this topic NOT FOUND") return false; } ... ... } |
Then we use a setTimeout to simulate internet activity.
First we get the array of subscribers by accessing the value by using topic as the index.
In an associative array, giving the topic key will give us the object value (array of subscribers).
Then we simply get the length of the subscribers.
By using the length, we use it to access the array of subscribers by using index. Remember, using strings in an associative array
will give you the object value associated with it.
By using numeric index, you’ll simply be giving the object value by index.
Hence after getting the length, we simply go through the subscriber array by looping through its index from length to 0, and
calling each subscriber’s callback. We can pass in whatever data we like for the publish.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
q.publish = function(topic, args) { if (!topics[topic]) { console.log("Error, object with this topic NOT FOUND") return false; } setTimeout(function() { var arrayOfSubscribers = topics[topic]; // get # of subscribers var len = arrayOfSubscribers ? arrayOfSubscribers.length : 0; // go through # of subscribers and execute their callback while (len--) { arrayOfSubscribers[len].func(topic, args, arrayOfSubscribers[len].token); } }, 500); return true; }; |
Using the pubsub object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
pubsub.subscribe("JS", function(topic, args, token) { console.log("Ricky received " + topic + " content"); console.log("The data is: " + args); console.log("Your token is: " + token); }); pubsub.subscribe("JS", function(topic, args, token) { console.log("David received JS data: " + args); console.log("Your token is: " + token); }); pubsub.subscribe("JS", function(topic, args) { console.log("EPAM has been notified of: " + topic); console.log("It is now heavily reading: " + args); }); pubsub.subscribe("JS", function() { console.log("Peter Ma has been notified"); }); pubsub.publish("JS", "Design Pattern of es6 JS"); |
Unsubscribe
First, we go through all the topics available in our topics associative array.
For each of these topics, there is a list of subscribers.
For each element of the array subscribers, we check to see if the subscriber has the token.
If so, we remove that subscriber from the subscribers’ array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
q.unsubscribe = function(token) { console.log(`unsubscribing subscriber with uid of ${token}`) for (let m in topics) { console.log(`For topic ${m}, let us check for any subscribers with token ${token}`) let subscribers = topics[m]; if (subscribers) { for (let i = 0; i < subscribers.length; i++) { if (subscribers[i].token === token) { console.log(`HO HO! FOUND A SUBSCRIBER with token ${token}. Let us remove it!`) subscribers.splice(i, 1); // remove 1 element at index i return token; } } } } return 0 }; |
FULL SOURCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
// global object of empty literal object var pubsub = {}; // block scope created by IIFE. // it takes in a parameter q (function(q){ // private literal object var topics = {}; // private id var subUid = -1; // Different objects will want to subscribe and receive notifications. // 1) the objects will call subscribe. It will then pass in the topic // to which it wants to subscribe // It will pass in a callback to receive the data q.subscribe = function(topic, func) { // given key 'topic', if there is no value, we initialize empty array // for that key if (!topics[topic]) { topics[topic] = []; } // a very simple id generator var token = (++subUid).toString(); // INSERT A SUBSCRIBER // the topic is the key property, Value is the literal object we push. topics[topic].push({ token: token, // token reps an id. func: func // func is the functionality we decide to pass in }); // return the key used for this object that's stored in in topics return token; }; // we add a function property to the passed in global object pubsub q.publish = function(topic, args) { if (!topics[topic]) { console.log("Error, object with this topic NOT FOUND") return false; } //setTimeout(function() { var arrayOfSubscribers = topics[topic]; // get # of subscribers var len = arrayOfSubscribers ? arrayOfSubscribers.length : 0; console.log(`# of subscribers: ${len}`) // go through # of subscribers and execute their callback while (len--) { //console.log(`Executing subscriber ${topic}, `) console.log(arrayOfSubscribers[len]); arrayOfSubscribers[len].func(topic, args, arrayOfSubscribers[len].token); } //}, 0); return true; }; q.unsubscribe = function(token) { console.log(`unsubscribing subscriber with uid of ${token}`) for (let m in topics) { console.log(`For topic ${m}, let us check for any subscribers with token ${token}`) let subscribers = topics[m]; if (subscribers) { for (let i = 0; i < subscribers.length; i++) { if (subscribers[i].token === token) { console.log(`HO HO! FOUND A SUBSCRIBER with token ${token}. Let us remove it!`) subscribers.splice(i, 1); // remove 1 element at index i return token; } } } } return 0 }; }(pubsub)); // we pass parameter pubsub into the IIFE let toUnsubscribe; pubsub.subscribe("JS", function(topic, args, token) { console.log("Ricky received " + topic + " content"); console.log("The data is: " + args); console.log("Your subscription token is: " + token); toUnsubscribe = token; }); pubsub.subscribe("JS", function(topic, args, token) { console.log("David received JS data: " + args); console.log("Your subscription token is: " + token); }); pubsub.publish("JS", "Design Pattern of es6 JS"); if (pubsub.unsubscribe(toUnsubscribe)>0) { console.log("removed subscription") } |