Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

James Robb
James Robb

Posted on

     

The Publisher Subscriber pattern

The Publisher Subscriber pattern, also known as PubSub, is an architectural pattern for relaying messages to interested parties through a publisher. The publisher is not generally aware of the subscribers per say but in our implementation it will so that we can ease into the topic.

The PubSub pattern gives us a scalable way to relay messages around our applications but is inflexible in one area and that is the data structure that is sent to each subscriber when a new message is published. Generally this is a good thing though in my opinion since it allows a nice normalised way of transacting data through our applications.

Tests

For the tests I will be using JavaScript andthe Jest test runner.

constPublisher=require('./publisher');letpublisher;beforeEach(()=>publisher=newPublisher);describe("Publisher",()=>{it("Should construct with default values",()=>{expect(publisher.topic).toEqual("unknown");expect(publisher.subscribers).toEqual([]);});it("Should add subscribers properly",()=>{constsubscriber=jest.fn();expect(publisher.subscribers.length).toEqual(0);publisher.subscribe(subscriber);expect(publisher.subscribers.length).toEqual(1);});it("Should publish updates to subscribers",()=>{constsubscriber=jest.fn();publisher.subscribe(subscriber);publisher.publish("test");expect(subscriber).toHaveBeenCalledWith({topic:"unknown",data:"test"});});it("Should unsubscribe from updates as required",()=>{constsubscriber=jest.fn();constsubscription=publisher.subscribe(subscriber);publisher.publish("test");expect(subscriber).toHaveBeenCalledTimes(1);publisher.unsubscribe(subscription);publisher.publish("test");expect(subscriber).toHaveBeenCalledTimes(1);});it("Should not unsubscribe a subscriber from updates unless it exists",()=>{constsubscriber=jest.fn();publisher.subscribe(subscriber);expect(publisher.subscribers.length).toEqual(1);publisher.unsubscribe(()=>24);expect(publisher.subscribers.length).toEqual(1);});it("Generates a consistent subscription id for each subscriber",()=>{constsubscriber=jest.fn();constsubscription=publisher.subscribe(subscriber);constproof=publisher.createSubscriptionId(subscriber);expect(subscription).toEqual(proof);});});
Enter fullscreen modeExit fullscreen mode

Here we test that:

  1. We begin with sane defaults
  2. We can add subscribers
  3. We can notify subscribers
  4. We can remove subscribers
  5. We only remove subscribers when they exist
  6. We generate consistent ids for each subscriber that is provided

You can run the tests here:

This covers the bases required of a publisher and subscriber and gives us control over who does and does not get notifications when new content is published. Pretty simple so far, right?

Implementation

For our implementation I will be usingTypeScript, a typed superset of JavaScript. If you are more comfortable with JavaScript you cancompile TypeScript code to JavaScript in the TypeScript playground.

exportinterfaceISubscriberOutput{topic:string;data:any;};exportclassPublisher{publictopic:string="unknown";privatesubscribers:Function[]=[];publicsubscribe(subscriberFn:Function):number{this.subscribers=[...this.subscribers,subscriberFn];constsubscriptionId=this.createSubscriptionId(subscriberFn);returnsubscriptionId;}publicpublish(data:any):void{this.subscribers.forEach((subscriberFn:Function)=>{constoutput:ISubscriberOutput={topic:this.topic,data};subscriberFn(output);});}publicunsubscribe(subscriptionId:number):void{constsubscriberFns=[...this.subscribers];subscriberFns.forEach((subscriberFn:Function,index:number)=>{if(this.createSubscriptionId(subscriberFn)===subscriptionId){subscriberFns.splice(index,1);this.subscribers=[...subscriberFns];}});}privatecreateSubscriptionId(subscriberFn:Function):number{constencodeString=this.topic+subscriberFn.toString();return[...encodeString].reduce((accumulator,char)=>{returnchar.charCodeAt(0)+((accumulator<<5)-accumulator);},0);}}
Enter fullscreen modeExit fullscreen mode

This class generates a Publisher with a set of methods for us to use for publishing updates, subscribing to those updates and also unsubscribing when the need arises. Let's break things down from top to bottom.

exportinterfaceISubscriberOutput{topic:string;data:any;};
Enter fullscreen modeExit fullscreen mode

This interface is able to be used by subscribers that will take in messages when thepublish method is called on thePublisher and gives us the structured message output we discussed in the introduction of this article.

publictopic:string="unknown";privatesubscribers:Function[]=[];
Enter fullscreen modeExit fullscreen mode

As we begin to define thePublisher class, we first initialise the class with a topic of "unknown" since the topic hasn't been provided or overridden. We also have an array ofsubscribers initialised, each of which should be aFunction.

Next we create thesubscribe method. This will add the providedsubscriberFn function to thesubscribers array and then return asubscriptionId for us to use later should we choose to unsubscribe down the road.

publicsubscribe(subscriberFn:Function):number{this.subscribers=[...this.subscribers,subscriberFn];constsubscriptionId=this.createSubscriptionId(subscriberFn);returnsubscriptionId;}
Enter fullscreen modeExit fullscreen mode

ThecreateSubscriptionId generates a unique ID for each subscriber and utilises the same algorithm as thethe Java String hashCode() Method.

privatecreateSubscriptionId(subscriberFn:Function):number{constencodeString=this.topic+subscriberFn.toString();return[...encodeString].reduce((accumulator,char)=>{returnchar.charCodeAt(0)+((accumulator<<5)-accumulator);},0);}
Enter fullscreen modeExit fullscreen mode

In short we take the currenttopic and add to that the string representation of thesubscriberFn. This gives us a somewhat unique string but is not bulletproof by any means. From here we take each character in theencodeString and reduce it to a number representation unique to that string.

Sidenote: We use the<< operator which seems to confuse a lot of developers but in this case we are just doing this in essence:accumulator * (2 ** 5).

So let's say it is the second iteration of thereduce loop and accumulator is at an arbitrary value of 65 which is the character code for lowercase "a".

This being the case the(accumulator << 5) - accumulator line is the same as writing(65 * (2 ** 5)) - 65 during that iteration.

Either way you write it, the resulting number will be 2015 for that example.

Hopefully that makes a bit more sense for you who may not have experienced using bitwise operators before.

I highly recommendreading the MDN bitwise reference for more information.

If we want to unsubscribe from aPublisher at any time, you can simply call theunsubscribe method passing in the return value of the originalsubscribe call.

publicunsubscribe(subscriptionId:number):void{constsubscriberFns=[...this.subscribers];subscriberFns.forEach((subscriberFn:Function,index:number)=>{if(this.createSubscriptionId(subscriberFn)===subscriptionId){subscriberFns.splice(index,1);this.subscribers=[...subscriberFns];}});}
Enter fullscreen modeExit fullscreen mode

Here we clone the current subscribers and loop over the clone until we find one that when it is hashed in thecreateSubscriptionId function, matches the providedsubscriptionId value.

If we find a match then we remove that function from thesubscriberFns array and set thesubscribers to contain only the remainingsubscriberFns.

Lastly we will look at thepublish function which takes in somedata which can be anything you wish to broadcast to thesubscribers.

publicpublish(data:any):void{this.subscribers.forEach((subscriberFn:Function)=>{constoutput:ISubscriberOutput={topic:this.topic,data};subscriberFn(output);});}
Enter fullscreen modeExit fullscreen mode

We loop over the currentsubscribers and notify each one with an object matching theISubscriberOutput structure.

Overall this implementation keeps things concise and to the point.

Example usage

An example use case could be an article publisher which notifies subscribers when new articles get published. It could look like this for example:

Conclusions

I like this pattern and how it allows a scalable and predictable messaging format and how flexible it can be to the needs of what you are building.

I think this ties in nicely with other architectural patterns like the microservices pattern which uses event queues to pass information around in a way that is not too dissimilar to PubSub.

Hopefully you found some value in todays post and you can make use of this pattern in the future!

Top comments(4)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
tweettamimi profile image
Tamimi
Just another developer chillin in an event-driven world of applications 🦄
  • Location
    Ottawa, Canada
  • Work
    Developer Advocate
  • Joined

I like how you explained it - thanks for sharing!

CollapseExpand
 
jamesrweb profile image
James Robb
I like to build cool things, work with nice people and help others where I can. Currently I'm an engineering manager for a fintech startup and historically a serial founder & freelancer software dev.
  • Location
    München, Deutschland 🇩🇪
  • Education
    The Open University
  • Work
    Engineering Manager @ Deutsche Fintech Solutions GmbH
  • Joined

You're welcome Tamimi, I'm glad you found value in the post, thanks for dropping by!

CollapseExpand
 
tweettamimi profile image
Tamimi
Just another developer chillin in an event-driven world of applications 🦄
  • Location
    Ottawa, Canada
  • Work
    Developer Advocate
  • Joined

I'm recently getting more into pub/sub and event-driven development, I attempted to build an event-driven NodeJS application on covid data that I wrote a blog post about here if you want to check it outdev.to/tweettamimi/how-i-built-an-...! It's pretty cool and I'm planing to build more side projects with it

Thread Thread
 
jamesrweb profile image
James Robb
I like to build cool things, work with nice people and help others where I can. Currently I'm an engineering manager for a fintech startup and historically a serial founder & freelancer software dev.
  • Location
    München, Deutschland 🇩🇪
  • Education
    The Open University
  • Work
    Engineering Manager @ Deutsche Fintech Solutions GmbH
  • Joined

I'll be sure to check that out, thanks for the share.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I like to build cool things, work with nice people and help others where I can. Currently I'm an engineering manager for a fintech startup and historically a serial founder & freelancer software dev.
  • Location
    München, Deutschland 🇩🇪
  • Education
    The Open University
  • Work
    Engineering Manager @ Deutsche Fintech Solutions GmbH
  • Joined

More fromJames Robb

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp