The Power of the Observer Pattern in JavaScript

jsmanifest

jsmanifest

Posted on October 5, 2019

The Power of the Observer Pattern in JavaScript

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

And then we attach this interface directly on a property of a subject:

function Subject() {
  this.observers = new ObserversList()
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

You might have realized that the method notifyTicket is missing, since we used it here:

person.notifyTicket(ticketNum, function accept() {
Enter fullscreen mode Exit fullscreen mode

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]
  }
}
Enter fullscreen mode Exit fullscreen mode

And here is the definition for WaitingListPerson:

function WaitingListPerson(ticketNum) {
  this.ticketNum = ticketNum

  this.notifyTicket = function(num, accept) {
    if (this.ticketNum === num) {
      accept()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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())
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

observer pattern in javascript

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

💖 💪 🙅 🚩
jsmanifest
jsmanifest

Posted on October 5, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related