The Power of Chain Of Responsibility in JavaScript

Christopher T.

April 18th, 2021

Share This Post

Design Patterns are integral to know in the software industry as they have been proven to solve real life problems in enterprise applications. A common pattern used is the publish/subscribe pattern which is extensively used in the DOM. The command pattern is used in Redux which had boomed (even to this day) in a short period of time due to its robust ability to be manage app state while being scalable in an easy and predictable manner.

The interesting part about design patterns is that they aren't a "one size fits all" solution. Some design patterns can work more efficient than others depending on the scenario and its up to the developer to decide when and how to use them more efficiently in their apps.

In this article, we will be going over the Chain of Responsibility design pattern by implementing one and talk about some use cases.

The Chain of Responsibility (COR) is a pattern that allows some request to be sent, received, and handled by multiple objects. These objects (which are just functions) are not dependent on the implementation details of the previous nor the next request and can decide what to do when it runs its execution. They can also either abort the whole chain or decide to let the request continue on to the next object (or function) in the chain.

Here is an example of the pattern used in DOM:

<div id="root" onclick="onBtnContainerClick()">
  <button>Say hello</button>
</div>
const btn = document.querySelector('button')

btn.addEventListener('click', function sayHello(e) {
  console.log('hello!!')
})

function onBtnContainerClick() {
  console.log('Clicked btnContainer')
}

When you click the button, the listener attached to the button is called. When the function ends, the "request" is then passed (or bubbled) to its parent:

chain-of-responsibility-dom1

It can decide to stop this from continuing (or bubbling) to its parent by calling e.stopPropagation():

chain-of-responsibility-dom2

const btn = document.querySelector('button')

btn.addEventListener('click', function sayHello(e) {
  e.preventDefault()
  console.log('hello!!')
})

function onBtnContainerClick() {
  console.log('Clicked btnContainer')
}

There's not really an "official" or correct way to implement this pattern as long as the functions can chain one after another in a controllable and predictable way. Generally though there are two main roles that define it which is worth mentioning:

  1. Handler - Determines the interface where the requests are handled. It also handles the requests that are linked together in the chain
  2. Client - Initiates the request to a handler in the chain

Here's a variation of the pattern taken from a gist:

class CumulativeSum {
  constructor(intialValue = 0) {
    this.sum = intialValue
  }

  add(value) {
    this.sum += value
    return this
  }
}

Source

The chain occurs when the add function returns this:

const sum1 = new CumulativeSum()
console.log(sum1.add(10).add(2).add(50).sum) // 62

This means it can be re-invoked as much as want it to since the instance is returned again at the end of the call:

add(value) {
  this.sum += value;
  return this;
}

The responsibility of how to break or continue the chain is given to each add function block. In our example, we have one add method that sums its value in one way:

this.sum += value

What happens inside the add method can literally be anything and it doesn't have to depend on anything that is outside of its function scope. For example, we can make use of this with a chain of API requests where each request in the chain can compute a new sum using their own logic, and pass the result of the previous sum to the next function in the chain that is interested in working with it.

Pretend we are in a situation where we are selling pets from our imaginary warehouse. We need to take a sum of all the pets that are on sale in our warehouse so that we can determine if we need to re-stock on them earlier than usual due to an emergency statewide stay-at-home order. We're doing this because people will most likely suffer from loneliness and depression so we expect that sales will be booming next couple of months:

class CumulativeSum {
  constructor() {
    this._executor = null
  }

  get executor() {
    if (!this._executor) return (val) => val
    return this._executor
  }

  set executor(fn) {
    this._executor = fn
  }

  async add(currentValue) {
    const result = await this.executor(currentValue, async (nextVal) => {
      if (this.next) return this.next.add(nextVal)
      return nextVal
    })
    return result
  }
}

Our extended CumulativeSum retains its ability to achieve the same goal but it now allows us to customize the behavior on how to determine the next sum.

Below we have a PetSum class that holds a sum. It will have its own add method that initiates any CumulativeSum by calling their add method (this will call all of the chain of requests that are linked after it). If you recall in the beginning of the post the term "Handler" is our PetSum below:

class PetsSum {
  constructor() {
    this.totalPets = 0
  }

  async add(summee) {
    this.totalPets = await summee.add(this.totalPets)
    return this.totalPets
  }
}

const reqSum = new ReqSum()

And here's an example where 4 CumulativeSum's are created which will be linked together, each representing a different animal species because our warehouse has multiple species:

const catsStorage = new CumulativeSum()
const dogsStorage = new CumulativeSum()
const squirrelsStorage = new CumulativeSum()
const birdsStorage = new CumulativeSum()

Each of them can have their own way of calculating the sum of their species or even choose not to do anything at all:

catsStorage.executor = async (val, next) => {
  const catsUrl = new URL('https://catstorage.com/api/catcount')
  // Making some API request. Pretend the response was 3
  const totalCats = 3
  if (next) return next(val + totalCats)
  return val + totalCats
}

dogsStorage.executor = async (val, next) => {
  const totalDogs = 2

  if (next) return next(val + totalDogs)
  return val + totalDogs
}

squirrelsStorage.executor = async (val, next) => {
  const totalSquirrels = 10
  if (next) return next(val + totalSquirrels)
  return val + totalSquirrels
}

birdsStorage.executor = async (val, next) => {
  const totalBirds = 1
  if (next) return next(val + totalBirds)
  return val + totalBirds
}

catsStorage.next = dogsStorage
dogsStorage.next = squirrelsStorage
squirrelsStorage.next = birdsStorage

To get the total, all that needs to be done is to call the add method on one of the instances:

const getTotalPets = async (summee) => petSum.add(summee)

getTotalPets(catsStorage)
  .then((totalPets) => {
    console.log(`Total pets now: ${totalPets}`)
    // Total pets now: 16
  })
  .catch(console.error)

See how easy that was?

We can start the request chain from one of the instances in between, for example we can just start with the dogs and forget about the cats (remember that catsStorage was the first request, but we can just start with dogsStorage and continue the chain of requests from there):

const getTotalPets = async (summee) => petSum.add(summee)

getTotalPets(dogsStorage)
  .then((totalPets) => {
    console.log(`Total pets now: ${totalPets}`)
    // Total pets now: 13
  })
  .catch(console.error)

This is useful when we want to make some switch to turn off one or the other anytime we want, like when a customer made an appointment to purchase all of our squirrels on a certain day like March 23 between 1PM and 2PM:

const isTodayThatDayBetween1and2PM = (date) => {
  // Just pretend we have some full-blown implementation
  return true
}

const getTotalPets = async (summee) => {
  if (isTodayThatDayBetween1and2PM()) {
    dogsStorage.next = birdsStorage
  }
  return petSum.add(summee)
}

getTotalPets(catsStorage)
  .then((totalPets) => {
    console.log(`Total pets now: ${totalPets}`)
    // Total pets now: 6
  })
  .catch(console.error)

If you are familiar with the Linked List data structure, you might realize that they are strikingly similar. And they are! Any operation you can do with a linked list can also be done in our examples. We can traverse each function in the chain including backwards by also attaching something like a prev (for previous) property on each one:

class PetsSum {
  constructor() {
    this.totalPets = 0
  }

  async add(summee) {
    this.totalPets = await summee.add(this.totalPets)
    return this.totalPets
  }

  petSum.forEach(summee, callback) {
    let currIndex = 0
    let currSummee = summee

    while (currSummee) {
      callback(currSummee, currIndex)
      currSummee = currSummee.next
      currIndex++
    }
  }
}

const catsStorage = new CumulativeSum('Cats')
const dogsStorage = new CumulativeSum('Dogs')
const squirrelsStorage = new CumulativeSum('Squirrels')
const birdsStorage = new CumulativeSum('Birds')
const frogsStorage = new CumulativeSum('Frogs')
const rabbitsStorage = new CumulativeSum('Rabbits')
const fishStorage = new CumulativeSum('Fishes')

catsStorage.next = dogsStorage
dogsStorage.next = squirrelsStorage
squirrelsStorage.next = birdsStorage
birdsStorage.next = frogsStorage
frogsStorage.next = rabbitsStorage
rabbitsStorage.next = fishStorage

forEach(catsStorage, (summee, index) => {
  console.log([summee.name, index])
})

Result:

forEach chain of responsibility

Which real world scenarios would this be used?

Here are some scenarios where the COR pattern fits nicely with:

  1. Coffee Maker - A coffee maker can become programmed with the COR pattern where each request/handler in the chain defines a separate ingredient to add into the machine. The flow closely follows how we are already making coffee today where can stop adding ingredients at any time or can choose to continue with the espresso, milk, almond crumb toppings, etc.
  2. ATM Machine - When choosing the option to withdraw $100 in $20 bills, the machine dispenses five $20 bills until it reaches the requested amount where the operation ends. This behavior is achievable in the
  3. Transformers - Utilize a chain of transformer/parsers in some custom AST (I literally just wrote this as I was writing this artice so this is not a full-blown battle-tested or optimized code, but I ran the code and it works nicely):
const handleColors = (obj, key, value, next) => {
  if (typeof value === 'string' && value.startsWith('0x')) {
    obj[key] = value.replace('0x', '#')
  }
  next()
}

const handleSizes = (obj, key, value, next) => {
  if (/(width|height|fontSize)/.test(key)) {
    const isDec = Number(value) % 1 !== 0
    obj[key] = String(value).endsWith('px')
      ? value
      : `${String(isDec ? value * 100 : value)}px`
  }
  next()
}

const handleAlign = (obj, key, value, next) => {
  switch (key) {
    case 'align':
      if (value === 'centerX') {
        obj.textAlign = 'center'
        delete obj.align
      }
      break
    case 'textAlign':
      if (value !== null && typeof value === 'object') {
        if (value.y) {
          obj.display = 'flex'
          if (value.y === 'center') {
            Object.assign(obj, { display: 'flex', alignItems: 'center' })
            if (value.x === 'center') {
              obj.justifyContent = 'center'
            }
            if (value.y === 'bottom') {
              obj.justifyContent = 'flex-end'
            }
          }
          if (value.x === 'center') obj.textAlign = 'center'
          else delete value.textAlign
        }
      }
      break
  }
  next()
}

class TNode {
  next = null

  constructor(callback) {
    this.callback = callback
  }

  execute(...args) {
    return this.callback(...args, () => {
      this.next && this.next.execute(...args)
    })
  }
}

function createTransform(...transformers) {
  function withNode(transformer) {
    return new TNode(transformer)
  }

  transformers = transformers.slice().reverse()

  let executor = withNode(transformers.pop())
  let curr = executor

  while (transformers.length) {
    curr.next = withNode(transformers.pop())
    curr = curr.next
  }

  function transform(obj) {
    for (const [key, value] of Object.entries(obj)) {
      executor.execute(obj, key, value)
    }
    return obj
  }

  return transform
}

const rawData = {
  backgroundColor: '0x020303',
  color: '0x201313',
  fontSize: '14',
  width: '0.9',
  height: '0.50',
  textAlign: {
    x: 'center',
    y: 'center',
  },
}

const transform = createTransform(handleColors, handleSizes, handleAlign)

const result = transform(rawData)

console.log(result)

Result:

{
  "backgroundColor": "#020303",
  "color": "#201313",
  "fontSize": "14px",
  "width": "90px",
  "height": "50px",
  "textAlign": "center",
  "display": "flex",
  "alignItems": "center",
  "justifyContent": "center"
}

In all of or transformer functions we call the next callback which is a function we created to call the next function in the chain.

We can choose to not continue the chain anytime we want to inside the function. For example we can stop before the handleAlign function gets called if textAlign is not an object (an object would be invalid in a DOM element):

const handleSizes = (obj, key, value, next) => {
  if (/(width|height|fontSize)/.test(key)) {
    const isDec = Number(value) % 1 !== 0
    obj[key] = String(value).endsWith('px')
      ? value
      : `${String(isDec ? value * 100 : value)}px`
  }
  if (typeof obj.textAlign === 'object') {
    next()
  }
}

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!


Tags


Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2021