How JavaScript works: the publisher-subscriber pattern
This is post # 47 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript tool for developers to identify, visualize, and reproduce web app bugs through pixel-perfect session replay.
Introduction
The ability to identify recurring patterns or themes in our code is an essential requirement for its optimization. And this is where the understanding of design patterns is invaluable.
Design patterns are reusable solutions to common software design problems.
It is important to note that design patterns are not complete solutions themselves but they provide us with a solution scheme. We can think of them as templates for solving software design problems.
Design patterns give us proven approaches to common software development problems and since they are language agnostic, they can be used and reused by anyone with any language.
Although they reflect the experience of the developers that defined them, they are extensible. Thus other developers can adapt them to suit their needs.
The concept of design pattern has long been in programming but in a less formal form. However, in 1995, a group popularly known as the gang of four or GoF published their book: Design Patterns: Elements of Reusable Object-Oriented Software.
This GoF publication fostered the popularity of design patterns in software development and it is often regarded as the most iconic design pattern publication to date.
In their book, they provided twenty-three object-oriented patterns that are in use to date as well as a number of software development techniques and pitfalls.
You can learn more about these patterns and why they are needed in a previous article that talks more generally about design patterns.
In this article, Our focus is on the publisher-subscriber pattern. Let’s get started with an overview in the next section.
The observer pattern
The observer pattern is a design pattern in which you define a one-to-many relationship from one object known as the subject to many other objects known as the observers.
These observers can be functions that watch the subject and wait for a signal or notification from the subject before they run.
In the observer pattern when an object is modified, it notifies its dependent objects that changes have occurred to its state. An example is the model-view-controller architecture. When the view updates the model changes.
Event handlers are another example of the observer pattern. Event handlers are functions that listen for a specific event and they only run when that event is fired.
The observer pattern consists of a subject that keeps track of all the observers that are currently subscribed to it. The subject also contains methods that facilitate adding, removing, and notifying these dependent objects.
Below is a simple implementation of the observer pattern:
In the code above, the Subject
constructor maintains a list of observers. And we added the relevant methods to the prototype of the Subject
. This enables us to share these methods with every instance of the Subject
.
If you do not understand how we can share methods using an object’s prototype, I suggest you read a previous article that covers prototypal inheritance in JavaScript.
In the code above, the subscribe
method adds observers to the observers
array while the unsubscribe
method removes observers from the observers
array. The notify
method would notify the specified subscribed observer while the notifyAll
method notifies all the subscribed observers.
With the observer pattern, each observer is required to subscribe to the object firing the event — — the subject before it can get a notification. However, the pub/sub pattern does not require this. The pub/sub pattern uses a middleware that sits between the objects firing the event. In this case, the publishers and the subscribed objects or the subscribers. Let’s take a deep dive into the pub/sub design pattern in the next section.
Deep dive into the publisher-subscriber pattern
The pub/sub pattern involves a middleware that is also referred to as the pub/sub broker. The pub/sub broker handles interactions between the publishers and the subscribers. Publishers publish contents or publications to the pub/sub broker and it handles the delivery of these contents to the appropriate subscriber.
The pub/sub broker also enables loose decoupling of publishers and subscribers and it supports many to many relationships between the publishers and the subscribers.
So unlike the observer pattern, the pub/sub pattern allows multiple publishers and multiple subscribers.
In the pub/sub pattern a publisher publishes contents to a topic and interested subscribers access these contents by sending subscriptions to the pub/sub broker to subscribe to that topic. Also, unlike the observer pattern, both the publishers and the subscribers do not need to be aware of each other.
Consider the image below:
There are many implementations of the pub/sub patterns, some examples are IBM Websphere MQ, RabbitMQ, and RocketMQ , Apache Kafka, and Google Cloud Pub/Sub, and Pushy.
In this article, we will implement a basic system using JavaScript.
The JavaScript language is well suited for the pub/sub pattern because at its core most ECMAScript implementations are event-driven.
Our pub/sub implementation consists of a pub/sub class
that contains an events array that is used to maintain the list of all published events.
Also, the pub/sub class
has a subscription
method that handles all interactions between the publishers and subscribers.
Let’s see the implementation below:
In our small example above, the subscription
method returns an object containing a subscribe
method used to handle subscriptions and an unsubscribe
method that handles unsubscriptions.
Lastly, our pub/sub class
contains a publish
method that takes a variable number of arguments and invokes all the functions that are subscribed to the specified event with these arguments, using apply
. You can learn more about how apply
works by reading a previous article in this series.
Advantages of the publish/subscribe pattern
The loose decoupling of the Pub/sub pattern makes it suitable for many software engineering problems. It is highly scalable and well-fitted for distributed architectures such as microservices.
Pub/sub shines when building event notifications, distributed caching, distributed logging, and multi-data source systems.
Some examples of real-life applications that use the pub/sub pattern are Redis, Split, Twillo, and Gutenberg created by Netflix.
Pub/sub pattern vs Observer pattern vs data binding
We have learned above that the main difference between the pub/sub and the observer pattern is that the pub/sub pattern offers complete decoupling of the subscribers from the publishers. In the observer pattern, however, the observers or subscribers must be aware of the subject — also known as the observables.
Also, the observer pattern does not have a broker and the observables emit the notifications themselves.
Data binding is a general term. In a nutshell, it simply means “the value of property X
on object Foo
is semantically bound to the value of property Y
on object Bar
. There are no assumptions on how Foo
knows or is fed changes on object Bar
.
Data bind can be implemented using either the pub/sub or observer pattern. And the data is the Publisher/Observable.
Conclusion
The pub/sub and observer patterns are design patterns that every developer should be familiar with.
They encourage us to think hard about the relationships between the different components of our application. And they help us to organize our application components into smaller loosely coupled units that are easier to maintain and reuse.
SessionStack utilizes pub/sub services in order to process all of the ingested behavioral data from the browser in real-time. As the data is being ingested, SessionStack allows you to watch user sessions as videos, allowing you to see exactly what happened during their journey.
Combining this visual information with all of the tech data from the browser such as errors, stack traces, network issues, debug data, etc. you can easily understand problematic areas in your product and efficiently resolve them.
There is a free trial if you’d like to give SessionStack a try.
If you missed the previous chapters of the series, you can find them here:
- An overview of the engine, the runtime, and the call stack
- Inside Google’s V8 engine + 5 tips on how to write optimized code
- Memory management + how to handle 4 common memory leaks
- The event loop and the rise of Async programming + 5 ways to better coding with async/await
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript
- The building blocks of Web Workers + 5 cases when you should use them
- Service Workers, their life-cycle, and use case
- The mechanics of Web Push Notifications
- Tracking changes in the DOM using MutationObserver
- The rendering engine and tips to optimize its performance
- Inside the Networking Layer + How to Optimize Its Performance and Security
- Under the hood of CSS and JS animations + how to optimize their performance
- Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
- The internals of classes and inheritance + transpiling in Babel and TypeScript
- Storage engines + how to choose the proper storage API
- The internals of Shadow DOM + how to build self-contained components
- WebRTC and the mechanics of peer to peer connectivity
- Under the hood of custom elements + Best practices on building reusable components
- Exceptions + best practices for synchronous and asynchronous code
- 5 types of XSS attacks + tips on preventing them
- CSRF attacks + 7 mitigation strategies
- Iterators + tips on gaining advanced control over generators
- Cryptography + how to deal with man-in-the-middle (MITM) attacks
- Functional style and how it compares to other approaches
- Three types of polymorphism
- Regular expressions (RegExp)
- Introduction to Deno
- Creational, Structural, and Behavioural design patterns + 4 best practices
- Modularity and reusability with MVC
- Cross-browser testing + tips for prerelease browsers
- The “this” variable and the execution context
- High-performing code + 8 optimization tips
- Debugging overview + 4 tips for async code
- Deep dive into call, apply, and bind
- The evolution of graphics
- Dockerizing a Node.js application
- A deep dive into decorators
- Best practices for data compliance
- Proxy and Reflect
- SVG and its use cases (part 1)
- Class static blocks + 6 proposed semantics
- Introduction to Graphs and Trees
- Introduction to PM2, Strongloop, and Forever + 4 tips for Production Process Managers
- Аdvanced SVG capabilities (part 2)