The Publisher/Subscriber pattern, or “PubSub” for short, is a pattern that allows us to create modules that can communicate with each other without depending directly on each other.
This way either part can change completely and not affect the other. Additionally, this works for synchronous and asynchronous operations. Meaning that two completely different applications can also use the same pattern to communicate.
The following runs in Node.js, but by changing the import/export strategy can easily run in a browser. It shows how one object adds items to a list at an interval while another prints the results of the list periodically.
Firstly the publisher should be constructed first (in publisher.js):
class Publisher {
constructor() {
this.listeners = [];
}
addListener(callback) {
this.listeners.push(callback);
}
notify(message) {
for (const listener of this.listeners) {
listener(message);
}
}
}
module.exports = new Publisher();
This could be even simpler, but I opted to take an approach that support any number of publishers/subscribers — not just one.
Then the object to receive the notifications can be constructed (in printer.js):
'use strict';
const publisher = require('./publisher.js');
class Printer {
constructor() {
this.messages = [];
}
printOnInterval() {
setInterval(() => {
console.log(this.messages);
}, 1000);
}
addMessage(message) {
this.messages.push(message);
}
}
module.exports = {
init: () => {
const printer = new Printer();
publisher.addListener((message) => printer.addMessage(message));
printer.printOnInterval();
}
};
Finally, an object to create messages can be created that notifies the publisher called objectA.js:
'use strict';
const publisher = require('./publisher.js');
class ObjectA {
sendMessage() {
publisher.notify('Object A');
}
createMessagesOnInterval() {
setInterval(this.sendMessage, 1000);
}
}
module.exports = {
init: () => {
const objectA = new ObjectA();
objectA.createMessagesOnInterval();
}
};
Now they can all be run through a basic init script called init.js:
'use strict';
const printer = require('./printer.js');
const objectA = require('./objectA.js');
printer.init();
objectA.init();
The results of running node init.js should be something like this after a few seconds:
[]
[ ‘Object A’ ]
[ ‘Object A’, ‘Object A’ ]
[ ‘Object A’, ‘Object A’, ‘Object A’ ]
[ ‘Object A’, ‘Object A’, ‘Object A’, ‘Object A’ ]
The above is somewhat useful since objectA.js and printer.js are kept completely separate, though this pattern really starts being useful when many objects are used together.
For example here is **objectB.js **which is similar to objectA, but prints a different message at a faster rate:
'use strict';
const publisher = require('./publisher.js');
class ObjectB {
sendMessage() {
publisher.notify('Object B');
}
createMessagesOnInterval() {
setInterval(this.sendMessage, 500);
}
}
module.exports = {
init: () => {
const objectB = new ObjectB();
objectB.createMessagesOnInterval();
}
};
Now just initialize this in init.js and everything will just work:
'use strict';
const printer = require('./printer.js');
const objectA = require('./objectA.js');
const objectB = require('./objectB.js');
printer.init();
objectA.init();
objectB.init();
The results of running node init.js should be something like this after a few seconds:
[ ‘Object B’ ]
[ ‘Object B’, ‘Object A’, ‘Object B’, ‘Object B’ ]
[ ‘Object B’,
‘Object A’,
‘Object B’,
‘Object B’,
‘Object A’,
‘Object B’,
‘Object B’ ]
So the functionality of the system was increased without having to modify any other files than the newly created script and the init script. It is important to remember that the publisher can be extended to accept an object, more arguments and so on. Which makes this strategy applicable to any code.
#javascript #node-js