Find me on medium
In this post, we will be going over the Observer Pattern and implementing it with JavaScript so that hopefully you can attain a better understanding of it especially if you're having trouble understanding the concept.
The observer pattern remains one of the best practices for designing decoupled systems and should be an important tool for any JavaScript developer to use.
The observer pattern is a design pattern in which subjects (which are simply just objects with methods) maintain a list of observers who are "registered" to be notified of upcoming messages. When they receive some notification event about something from the subject they are attached to, they can use these opportunities to do something useful depending on what was received from the them.
The pattern is most useful in situations when you need multiple objects to get notified simultaneously at the same time of recent changes to state. Thus, the power of this pattern comes to light when you need multiple objects to maintain consistency throughout your app as opposed to having tightly coupled classes. With that said, it's even possible to have several objects that aren't directly related to each other to stay consistent at the same time.
Observers can remove themselves after they were attached, so there's even some flexibility on opting in and out for one observer and the next, and vice versa. When you have all of this functionality combined, you can build dynamic relationships between subjects and observers that make up robust functionality.
The concept goes like this:
When an observer is concerned about a subject's state and wants to opt in to "observe" upcoming state updates to it, they can register or attach themselves with them to receive upcoming information. Then, when something changes, those observers will be able to get notified of it including updates thereafter. This is done when the subject sends notification messages to its attached observer(s) using some broadcasting method. Each of these notification messages can contain useful data to one or more observers that receive them. The way that notify messages are sent is usually invoking some notify method to loop through its list of observers and inside each loop it would invoke the observer's update method. When the observer no longer wishes to be associated with the subject, they can be detached.
Here is a short and precise table with all of the common participants that make up this pattern:
Name | Description |
---|---|
Subject | Maintains observers. Can suggest the addition or removal of observers |
Observer | Provides an update interface for objects that need to be notified of a Subject’s changes of state |
ConcreteSubject | Broadcasts notifications to Observers on changes of state, stores the state of ConcreteObservers |
ConcreteObserver | Stores a reference to the ConcreteSubject, implements an update interface for the Observer to ensure state is consistent with the Subject’s |
Now let's go ahead and see how this might look like in code.
The first thing we are going to do is to begin creating the subject that will hold an interface for managing its observers. To do that, we are actually going to define the constructor on a separate function called ObserversList
:
function ObserversList() {
this.observers = []
}
ObserversList.prototype.add = function(observer) {
return this.observers.push(observer)
}
ObserversList.prototype.get = function(index) {
if (typeof index !== number) {
console.warn('the index passed in to getObserver is not a number')
return
}
return this.observers[index]
}
ObserversList.prototype.removeAt = function(index) {
this.observers.splice(index, 1)
}
ObserversList.prototype.count = function() {
return this.observers.length
}
ObserversList.prototype.indexOf = function(observer, startIndex = 0) {
let currentIndex = startIndex
while (currentIndex < this.observers.length) {
if (this.observers[currentIndex] === observer) {
return currentIndex
}
currentIndex++
}
return -1
}
ObserversList.prototype.notifyAll = function(data) {
const totalObservers = this.observers.length
for (let index = 0; index < totalObservers; index++) {
this.observers(index).update(data)
}
}
And then we attach this interface directly on a property of a subject:
function Subject() {
this.observers = new ObserversList()
}
We could have defined the prototyped methods directly on the subject, but the reason we don't is because the subjects are usually going to be arbitrary instances of something in a real world use case that just needs to inherit the observer interface, and then possibly extending its functionality or creating wrappers around them.
Now we will go ahead and define the Observer:
function Observer() {
this.update = function() {}
}
When different objects inherit the Observer, what usually happens is that they overwrite the update
(or some updater) function that is interested in some data that they were looking for.
This is because when the subject invokes its notifyAll
method, the observer's updater function is used on each loop.
You can see this in action above:
ObserversList.prototype.notifyAll = function(data) {
const totalObservers = this.observers.length
for (let index = 0; index < totalObservers; index++) {
// HERE
this.observers(index).update(data)
}
}
Real World Example
Let's now move on to a real world example.
Pretend that we are operating a DMV in the location Alhambra
. We're going to implement the ticket calling system using the observer pattern.
In a typical ticket calling system at the DMV, people are usually given a ticket number if they get placed into the waiting list and they'd wait until their number is called.
Right before they were given their ticket number, the DMV checks if there is already a booth available before handing it to them. If there are no booths available, that's when they get placed into the waiting list with their assigned ticket number.
When a person completes their session at the booth, let's pretend that they're done for the day. This is when their ticket number is no longer in use and can be re-used again later. In our example, we'll be marking the ticket numbers as immediately available to assign to someone else that will get placed into the waiting list.
The first thing we need to do is to define the DMV
constructor:
function DMV(maxTicketsToProcess = 5) {
this.ticketsFree = new Array(40).fill(null).map((_, index) => index + 1)
this.ticketsProcessing = []
this.maxTicketsToProcess = maxTicketsToProcess
this.waitingList = new WaitingList()
}
In our example, the DMV
is the subject because it's going to manage a list of people and ticket numbers.
We set a maxTicketsToProcess
parameter because without it, the waiting list will always be empty because we won't have a way to know when it's appropriate to place a person into the waiting list. When maxTicketsToProcess
is reached, we would start placing people into the waiting list with a ticket number if there are still tickets in this.ticketsFree
.
Now when we look at the DMV
constructor, it's assigning this.waitingList
with a WaitingList
instance. That WaitingList
is basically the ObserversList
as it provides a nearly identical interface to manage its list of people:
function WaitingList() {
this.waitingList = []
}
WaitingList.prototype.add = function(person) {
this.waitingList.push(person)
}
WaitingList.prototype.removeAt = function(index) {
this.waitingList.splice(index, 1)
}
WaitingList.prototype.get = function(index) {
return this.waitingList[index]
}
WaitingList.prototype.count = function() {
return this.waitingList.length
}
WaitingList.prototype.indexOf = function(ticketNum, startIndex) {
let currentIndex = startIndex
while (currentIndex < this.waitingList.length) {
const person = this.waitingList[currentIndex]
if (person.ticketNum === ticketNum) {
return currentIndex
}
currentIndex++
}
return -1
}
WaitingList.prototype.broadcastNext = function(ticketNum) {
const self = this
this.waitingList.forEach(function(person) {
person.notifyTicket(ticketNum, function accept() {
const index = self.waitingList.indexOf(person)
self.waitingList.removeAt(index)
delete person.processing
delete person.ticketNum
self.ticketsProcessing.push(ticketNum)
})
})
}
broadcastNext
is the equivalent of our notifyAll
method from the ObserversList
example. Instead of calling .update
however, we call .notifyTicket
that is defined on the person instance (which we will see in a bit) and provide an accept
callback function as the second argument because this will simulate the real life scenario when a person looks at their ticket number, realizes that their assigned number is being called and walks up to their booth
Lets define a Person
constructor to instantiate for each person:
function Person(name) {
this.name = name
}
You might have realized that the method notifyTicket
is missing, since we used it here:
person.notifyTicket(ticketNum, function accept() {
This is fine, because we don't want to mix in a waiting list's interface with a generic People
one.
So, we're going to create a WaitingListPerson
constructor that will contain its own interface specifically for people in the waiting list since we know that these functionalities won't be in any use after the person is taken out of it. So we keep things organized and simple.
The way we are going to extend instances of Person
is through a utility called extend
:
function extend(target, extensions) {
for (let ext in extensions) {
target[ext] = extensions[ext]
}
}
And here is the definition for WaitingListPerson
:
function WaitingListPerson(ticketNum) {
this.ticketNum = ticketNum
this.notifyTicket = function(num, accept) {
if (this.ticketNum === num) {
accept()
}
}
}
Great! The last thing we are going to do is to finally implement the methods to DMV
so that it will actually be able to add/remove people, manage ticket numbers, etc.
function DMV(maxTicketsToProcess = 5) {
this.ticketsFree = new Array(40).fill(null).map((_, index) => index + 1)
this.ticketsProcessing = []
this.maxTicketsToProcess = maxTicketsToProcess
this.waitingList = new WaitingList()
}
// Extracts ticket # from this.ticketsFree
// Adds extracted ticket # to this.ticketsProcessing
// Or add to this.waitingList
DMV.prototype.add = function(person) {
if (this.ticketsProcessing.length < this.maxTicketsToProcess) {
const ticketNum = this.ticketsFree.shift()
console.log(`Taking next ticket #${ticketNum}`)
this.processNext(person, ticketNum)
} else {
this.addToWaitingList(person)
}
}
// Appends "processing" and "ticketNum" to person
// Inserts ticket # to this.ticketsProcessing if holding ticketNum
DMV.prototype.processNext = function(person, ticketNum) {
person.processing = true
if (ticketNum !== undefined) {
person.ticketNum = ticketNum
this.ticketsProcessing.push(ticketNum)
}
}
// Extracts ticket # from this.ticketsFree
// Adds extracted ticket # to this.waitingList
DMV.prototype.addToWaitingList = function(person) {
const ticketNum = this.ticketsFree.splice(0, 1)[0]
extend(person, new WaitingListPerson(ticketNum))
this.waitingList.add(person)
}
// Extracts ticket # from this.ticketsProcessing
// Adds extracted ticket to this.ticketsFree
DMV.prototype.complete = function(person) {
const index = this.ticketsProcessing.indexOf(person.ticketNum)
this.ticketsProcessing.splice(index, 1)[0]
this.ticketsFree.push(person.ticketNum)
delete person.ticketNum
delete person.processing
if (this.waitingList.count() > 0) {
this.waitingList.broadcastNext(this.ticketsFree.shift())
}
}
Now we have a sufficient DMV ticketing system, backed by the observer pattern!
Let's try seeing this in use:
const alhambraDmv = new DMV()
const michael = new Person('michael')
const ellis = new Person('ellis')
const joe = new Person('joe')
const jenny = new Person('jenny')
const clarissa = new Person('clarissa')
const bob = new Person('bob')
const lisa = new Person('lisa')
const crystal = new Person('crystal')
alhambraDmv.add(michael)
alhambraDmv.add(ellis)
alhambraDmv.add(joe)
alhambraDmv.add(jenny)
alhambraDmv.add(clarissa)
alhambraDmv.add(bob)
alhambraDmv.add(lisa)
alhambraDmv.add(crystal)
const ticketsFree = alhambraDmv.ticketsFree
const ticketsProcessing = alhambraDmv.ticketsProcessing
console.log(`waitingNum: ${alhambraDmv.waitingList.count()}`)
console.log(
`ticketsFree: ${ticketsFree.length ? ticketsFree.map((s) => s) : 0}`,
)
console.log(`ticketsProcessing: ${ticketsProcessing.map((s) => s)}`)
console.log(michael)
console.log(ellis)
console.log(joe)
console.log(jenny)
console.log(clarissa)
console.log(bob)
console.log(lisa)
console.log(crystal)
alhambraDmv.complete(joe)
console.log(`waitingNum: ${alhambraDmv.waitingList.count()}`)
console.log(
`ticketsFree: ${ticketsFree.length ? ticketsFree.map((s) => s) : 0}`,
)
console.log(`ticketsProcessing: ${ticketsProcessing.map((s) => s)}`)
alhambraDmv.complete(clarissa)
console.log(michael)
console.log(ellis)
console.log(joe)
console.log(jenny)
console.log(clarissa)
console.log(bob)
console.log(lisa)
console.log(crystal)
So now we've seen how far the observer pattern can take your app. We've taken advantage of it to build a functional DMV ticket calling system!Give yourselves a pat on the back!
Conclusion
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!
Find me on medium
Top comments (2)
This is good for other languages which do not have event support, why would this be better than using EventTarget api?
developer.mozilla.org/en-US/docs/W...
I agree. This demonstration of the observer pattern suits languages other than JavaScript for broadcasting events, and is a great exercise in building out the pattern for oneself. But given that JS is an event-based runtime, much of the code here essentially reimplements parts of the event loop, but doesn't have the same depth of performance optimizations as the language builtins do