Publishing and subscribing, as we all know, is a classic design pattern. For example, popular libraries like Redux or front-end frameworks are used as communication mechanisms at the bottom, so today we will encapsulate a publish-and-subscription-based component.

Design a publish and subscribe library jpslib

The jpslib name stands for a simple publish-subscribe library that is the tool for our purposes. Build the tool, and then we’re going to use the tool to go from ape to member.

API design and implementation

  • First, we need to abstract a subscriber, just show me the code:

    /*** base class of subscriber*/jps.ISubscriber = function () {}; /*** must be implemented* interface to observe subscriber's interested notification*/jps.ISubscriber.prototype.listNotificationInterested = function () { return []; }; /*** must be implemented* interface to execute a notification interested by the subscriber*/jps.ISubscriber.prototype.executeNotification = function (notification) { };

The subscriber responsibilities listed the first is the theme of his interest in listNotificationInterested, the second is to receive executeNotification to subscribe to the theme of the message processing

  • Second, we abstract a message model, show me the code:

    /*** base class of notification*/ jps.INotification = function (name, body, type) { if(name === null || name === undefined) { throw new Error('notification must have a name'); } if(typeof name ! == 'string') { throw new TypeError('notification name must be string type'); } this._name = name; this._body = body; this._type = type; }; /*** interface to get notification's name*/jps.INotification.prototype.getName = function () { return this._name; }; /*** interface to get notification's body*/jps.INotification.prototype.getBody = function () { return this._body; }; /*** interface to get notification's type*/jps.INotification.prototype.getType = function () { return this._type; };

    The message object needs to have its specific name, message body and message type. In fact, we only need to know the message name, and the message type is only used for future extension.

  • Finally, there is the implementation of a manager

    /** * interface to subscribe notification */ jps.subscribe = function (subscriber) { if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function' && subscriber.executeNotification && typeof subscriber.executeNotification === 'function') { var names = subscriber.listNotificationInterested(); if(names instanceof Array) { //check names type names.forEach(function (name) { if(typeof name === 'string') { //do nothing } else { throw new Error('interested notification name must be String type'); }}); //clear jps._removeSubscribersFromMap(subscriber); //add names.forEach(function (name) { jps._addSubscriberToMap(name, subscriber); }); } else { throw new Error('interface listNotificationInterested of subscriber must return Array type'); } } else { throw new Error('subscriber must implement ISubscriber'); }}; /** * interface to publish notification */ jps.publish = function (notification) { if(notification.getName && typeof notification.getName === 'function') { var subs = jps._getSubscribersFromMap(notification.getName()).concat(); subs.forEach(function (ele) { ele.executeNotification(notification); }); } else { throw new Error('notification must implement INotification'); }}; /** * interface to create new notification object */ jps.createNotification = function (name, data, type) { if(typeof name === 'string') { var Notification = function (name, data, type) { jps.INotification.call(this, name, data, type); }; jps._utils.extendClass(Notification, jps.INotification); return new Notification(name, data, type); } else { throw new Error('notification name must be String type'); }}; /** * interface to unsubscribe notification interested by subscriber */ jps.unsubscribe = function (notificationname, subscriber) { if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function') {  if(typeof notificationname === 'string') { jps._removeSubscriberFromMap(notificationname, subscriber); } else { throw new Error('interested notification name must be String type'); } } else { throw new Error('subscriber must implement ISubscriber'); }}; /** * interface to unsubscribe all the notification interested by subscriber */ jps.unsubscribeAll = function (subscriber) { if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function') { if(typeof notificationname === 'string') { jps._removeSubscribersFromMap(subscriber); } else { throw new Error('interested notification name must be String type'); } } else { throw new Error('subscriber must implement ISubscriber'); }}; /** * the map from notification name to subscriber */ jps._notificationMap = {}; /** * get the subscribers from map by the notification name */ jps._getSubscribersFromMap = function (notificationname) { if(jps._notificationMap[notificationname] === undefined) { return []; } else { return jps._notificationMap[notificationname]; }}; /** * get the interested names from map by the subscriber */ jps._getInterestedNamesFromMap = function (subscriber) { var retArr = []; var arr; for(var name in jps._notificationMap) { arr = jps._notificationMap[name].filter(function (ele) { return ele === subscriber; }); if(arr.length > 0) { retArr.push(name); } } return retArr; }; /** * check the name is subscribed by the subscriber from map */ jps._hasSubscriberFromMap = function (name, subscriber) { var names = jps._getInterestedNamesFromMap(subscriber); var retArr = names.filter(function (ele) { return ele === name; }); return retArr.length > 0; }; /** * add the subscriber to the map */ jps._addSubscriberToMap = function (name, subscriber) { if(jps._hasSubscriberFromMap(name, subscriber)) { //do nothing } else { var subs = jps._getSubscribersFromMap(name); if(subs.length === 0) { subs = jps._notificationMap[name] = []; } subs.push(subscriber); } return true; }; /** * remove the subscriber from the map */ jps._removeSubscriberFromMap = function (name, subscriber) { var subs = jps._getSubscribersFromMap(name); var idx = subs.indexOf(subscriber); if(idx > -1) { subs.splice(idx, 1); } else { //do nothing } return subs; }; /** * remove all observed notification for the subscriber */ jps._removeSubscribersFromMap = function (subscriber) { var  subs; var names = jps._getInterestedNamesFromMap(subscriber); names.forEach(function (name) { var subs = jps._removeSubscriberFromMap(name, subscriber); if(subs && subs.length === 0) { delete jps._notificationMap[name]; }}); return true; }; jps._utils = { 'extendClass': function (child, parent) { if(typeof child ! == 'function') throw new TypeError('extendClass child must be function type'); if(typeof parent ! == 'function') throw new TypeError('extendClass parent must be function type'); if(child === parent) return ; var Transitive = new Function(); Transitive.prototype = parent.prototype; child.prototype = new Transitive(); return child.prototype.constructor = child; }};

The code here is a bit long, so let’s take a closer look:

  • The first is a tool objectjps._utilsThere are some helper methods in there, so far we need class inheritance.
  • The next is a key/value map:jps._notificationMapTo establish a mapping between the subscribed topic name and the subscriber.
  • Finally, the implementation of the manager.

    In these implementations, we see nothing more than adding, deleting, changing and checking. But we need to check for exceptions and handle them. And then there’s the convention of methods that begin with _, which generally means private, private members.

Encapsulated as the class library JPslib

Here, we can use tools like WebPack to handle the code. Finally published as NPM. The details are omitted and the code is available on Github.

Of course, here can use ES6 to write, it will be easier to read, I suggest readers to implement it, will have a profound experience.

Encapsulate a component base class

Let’s take the React component as an example:

'use strict'; import React from 'react'; import jpslib from 'jpslib'; class ComSubscriber extends jpslib.ISubscriber { constructor(name, callback, scope) { super(); this._name = name; this._callback = callback; this._scope = scope; } get name() { return this._name; } get callback() { return this._callback; } listNotificationInterested() { return [this._name]; } executeNotification(notice) { this._callback.call(this._scope || {}, notice.getBody()); }}class PSComponent extends React.Component { constructor(props) { super(props); this._subscribers = []; } hasSubscriber(name, callback) { for(let i = 0; i < this._subscribers.length; i++) { let sub = this._subscribers[i]; if(sub.name === name && sub.callback === callback) { return true; } } return false; } addSubscriber(name, callback) { if(typeof name === 'string') { if(typeof callback === 'function') { if(this.hasSubscriber(name, callback)) { return false; } let subscriber = new ComSubscriber(name, callback, this); jpslib.subscribe(subscriber); this._subscribers.push(subscriber); return true; } else { throw new Error('addSubscriber parameter callback should be type of function'); } } else { throw new Error('addSubscriber parameter name should be type of string'); } } removeSubscriber(name, callback) { if(typeof name === 'string' && typeof callback === 'function') { for(let i = 0; i < this._subscribers.length; i++) { let sub = this._subscribers[i]; if(sub.name === name && sub.callback === callback) { const subscriber = this._subscribers.splice(i, 1)[0]; jpslib.unsubscribe(name, subscriber); return true; } } } else if(typeof name === 'string' && callback === undefined) { for(let i = 0; i < this._subscribers.length; i++) { let sub = this._subscribers[i]; if(sub.name === name) { const subscirber = this._subscribers.splice(i, 1); jpslib.unsubscribe(name, subscriber); i--; } } return true; } return false; } sendNotification(name, body) { if(typeof name === 'string') { const notice = jpslib.createNotification(name, body); jpslib.publish(notice); } }}export default PSComponent;

All components that inherit from PSComponent can use jpslib communication, for example:

Create an enumeration of message names:

'use strict'; class NoticeTypes { static ICONBUTTON_CLICK = 'iconbutton_click'; }export default NoticeTypes;

Then the message subscription and callback processing:

'use strict'; import React from 'react'; import PSComponent from './pscomponent'; import ComNotice from './notice'; class App extends PSComponent { constructor(props) { super(props); } componentDidMount() { this.addSubscriber(ComNotice.ICONBUTTON_CLICK, this._iconbuttonClickHandler); } componentWillUnmount() { this.removeSubscriber(COMNotice.ICONBUTTON_CLICK, this._iconbuttonClickHandler); } _iconbuttonClickHandler() {console.log(' Help, I'm clicked '); } render() { ... }}; export default App;

To broadcast a message where it is needed:

this.sendNotification(COMNotice.ICONBUTTON_CLICK, {});

This encapsulation, isn’t it when we write code will find a lot easier to use, simple and clear.


Related project source, please visit Github. If there are mistakes, please kindly advise. Content are original, need to reprint, please contact me.