Combining the Command Pattern with State Pattern in JavaScript

jsmanifest

jsmanifest

Posted on November 24, 2022

Combining the Command Pattern with State Pattern in JavaScript

JavaScript is a popular language known for its flexibility. It's thanks to this that makes patterns like the command pattern easier to implement in our apps.

When there's a design pattern out there that goes well hand-in-hand with the state pattern, it's arguably the command pattern.

If you've read one of my previous blog posts about the state pattern you might remember this sentence: "The State Pattern ensures an object to behave in a predictable, coordinated way depending on the current "state" of the application."

In the command pattern the main goal is to separate the communication between two important participants:

  1. The initiator (also called the invoker)
  2. The handler

We are going to incorporate the command pattern together with the state pattern in this post. If you're learning either of the two, my best advice to you in order to get the most out of this post is to make sure you understand the flow of the state pattern before continuing to the command pattern implementation to better feel how the behavior of the code drastically changes while the functionality remains in tact.

Let's start off with an example using the state pattern so that we can see this in a clearer perspective:

let state = { backgroundColor: 'white', profiles: [] }
let subscribers = []

function notifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
}

function setBackgroundColor(color) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: color,
  }))
}

function setState(newState) {
  let prevState = state
  state =
    typeof newState === 'function'
      ? newState(prevState)
      : { ...prevState, ...newState }

  notifySubscribers(prevState, state)
}

function subscribe(callback) {
  subscribers.push(callback)
}

function addProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
}

subscribe(
  (function () {
    function getColor(length) {
      if (length >= 5 && length <= 8) return 'blue' // Average
      if (length > 8 && length < 10) return 'orange' // Reaching limit
      if (length > 10) return 'red' // Limit reached
      return 'white' // Default
    }

    return (prevState, newState) => {
      const prevProfiles = prevState?.profiles || []
      const newProfiles = newState?.profiles || []
      if (prevProfiles.length !== newProfiles.length) {
        setBackgroundColor(getColor(newProfiles.length))
      }
    }
  })(),
)

console.log(state.backgroundColor) // 'white'

addProfile({ id: 0, name: 'george', gender: 'male' })
addProfile({ id: 1, name: 'sally', gender: 'female' })
addProfile({ id: 2, name: 'kelly', gender: 'female' })

console.log(state.backgroundColor) // 'white'

addProfile({ id: 3, name: 'mike', gender: 'male' })
addProfile({ id: 4, name: 'bob', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

addProfile({ id: 5, name: 'kevin', gender: 'male' })
addProfile({ id: 6, name: 'henry', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

addProfile({ name: 'ronald', gender: 'male' })
addProfile({ name: 'chris', gender: 'male' })
addProfile({ name: 'andy', gender: 'male' })
addProfile({ name: 'luke', gender: 'male' })

console.log(state.backgroundColor) // 'red'
Enter fullscreen mode Exit fullscreen mode

In the example above we have a state and subscribers object. The subscribers object holds a collection of callback functions. These callback functions are called whenever the setState function is called:

state-pattern-notify-subscriber-callbacks-in-javascript

Every time the state updates, all of the registered callbacks are called with the previous state (prevState) and the new state (newState) in arguments.

We registered one callback listener so that we can watch updates to the state and update the background color whenever the amount of profiles hits a certain length. The table below shows a clearer picture lining up the profile counts with the associated color:

Minimum Threshold Background Color
0 white
5 blue
9 orange
10 red

state-pattern-subscribe-callback-handler-on-change

So how can the command pattern fit into this? Well, if we look back into our code we can see that we defined some functions responsible for both calling and handling this logic:

function notifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
}

function setBackgroundColor(color) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: color,
  }))
}

function addProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
}
Enter fullscreen mode Exit fullscreen mode

We can abstract these into commands instead. In the upcoming code example it will show the same code with the command pattern implemented in harmony with the state pattern. When we do that there are only 2 functions left untouched: setState and subscribe.

Let's go ahead and introduce the command pattern and commandify our abstracted functions:

let state = { backgroundColor: 'white', profiles: [] }
let subscribers = []
let commands = {}

function setState(newState) {
  let prevState = state
  state =
    typeof newState === 'function'
      ? newState(prevState)
      : { ...prevState, ...newState }

  dispatch('NOTIFY_SUBSCRIBERS', { prevState, newState: state })
}

function subscribe(callback) {
  subscribers.push(callback)
}

function registerCommand(name, callback) {
  if (commands[name]) commands[name].push(callback)
  else commands[name] = [callback]
}

function dispatch(name, action) {
  commands[name]?.forEach?.((fn) => fn?.(action))
}

registerCommand(
  'SET_BACKGROUND_COLOR',
  function onSetState({ backgroundColor }) {
    setState((prevState) => ({ ...prevState, backgroundColor }))
  },
)

registerCommand('NOTIFY_SUBSCRIBERS', function onNotifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
})

registerCommand('ADD_PROFILE', function onAddProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
})

subscribe(
  (function () {
    function getColor(length) {
      if (length >= 5 && length <= 8) return 'blue' // Average
      if (length > 8 && length < 10) return 'orange' // Reaching limit
      if (length > 10) return 'red' // Limit reached
      return 'white' // Default
    }

    return ({ prevState, newState }) => {
      const prevProfiles = prevState?.profiles || []
      const newProfiles = newState?.profiles || []
      if (prevProfiles.length !== newProfiles.length) {
        dispatch('SET_BACKGROUND_COLOR', {
          backgroundColor: getColor(newProfiles.length),
        })
      }
    }
  })(),
)

console.log(state.backgroundColor) // 'white'

dispatch('ADD_PROFILE', { id: 0, name: 'george', gender: 'male' })
dispatch('ADD_PROFILE', { id: 1, name: 'sally', gender: 'female' })
dispatch('ADD_PROFILE', { id: 2, name: 'kelly', gender: 'female' })

console.log(state.backgroundColor) // 'white'

dispatch('ADD_PROFILE', { id: 3, name: 'mike', gender: 'male' })
dispatch('ADD_PROFILE', { id: 4, name: 'bob', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

dispatch('ADD_PROFILE', { id: 5, name: 'kevin', gender: 'male' })
dispatch('ADD_PROFILE', { id: 6, name: 'henry', gender: 'male' })

console.log(state.backgroundColor) // 'blue'

dispatch('ADD_PROFILE', { id: 7, name: 'ronald', gender: 'male' })
dispatch('ADD_PROFILE', { id: 8, name: 'chris', gender: 'male' })
dispatch('ADD_PROFILE', { id: 9, name: 'andy', gender: 'male' })
dispatch('ADD_PROFILE', { id: 10, name: 'luke', gender: 'male' })

console.log(state.backgroundColor) // 'red'
Enter fullscreen mode Exit fullscreen mode

It's much clearer now determining which functions we need to update the state. We can separate everything else into their own separate command handlers. That way we can isolate them to be in their own separate file or location so we can work with them easier.

Here were the steps demonstrated in our updated example to get to that point:

  1. Create the commands variable. This will store registered commands and their callback handlers.
  2. Define registerCommand. This will register new commands and their callback handlers to the commands object.
  3. Define dispatch. This is responsible for calling the callback handlers associated with their command.

With these three steps we perfectly set up our commands to be registered by the client code, allowing them to implement their own commands and logic. Notice how how our registerCommand and dispatch functions don't need to be aware of anything related to our state objects.

We can easily take advantage of that and continue with isolating them into a separate file:

commands.js

// Private object hidden within this scope
let commands = {}

export function registerCommand(name, callback) {
  if (commands[name]) commands[name].push(callback)
  else commands[name] = [callback]
}

export function dispatch(name, action) {
  commands[name]?.forEach?.((fn) => fn?.(action))
}
Enter fullscreen mode Exit fullscreen mode

As for the actual logic written in these lines:

registerCommand(
  'SET_BACKGROUND_COLOR',
  function onSetState({ backgroundColor }) {
    setState((prevState) => ({ ...prevState, backgroundColor }))
  },
)

registerCommand('NOTIFY_SUBSCRIBERS', function onNotifySubscribers(...args) {
  subscribers.forEach((fn) => fn(...args))
})

registerCommand('ADD_PROFILE', function onAddProfile(profile) {
  setState((prevState) => ({
    ...prevState,
    profiles: prevState.profiles.concat(profile),
  }))
})
Enter fullscreen mode Exit fullscreen mode

Normally this would all be left to the client code to decide that (technically, our last code snippet are representing the client code). Some library authors also define their own command handlers for internal use but the same concept still applies. One practice I see often is having their internal logic put into a separate file with its file name prefixed with "internal" (example: internalCommands.ts). It's worth noting that 99% of the time the functions in those files are never exported out to the user. That's why they're marked internal.

The image below is a diagram of what our code looked like before implementing the command design pattern to it:

state-pattern-before-command-pattern-integration

The bubbles in the purple represent functions. The two functions setBackgroundColor and addProfile are included amongst them. Specifically for those two however call setState directly to facilitate changes to state. In other words they both call and handle the state update logic to specific slices of the state they're interested in.

Now have a look at the diagram below. This image shows how our code looks like after implementing the pattern:

command-design-pattern-together-with-state-pattern

The functions notifySubscribers, addProfile, and setBackgroundColor are gone, but all of their logic still remains. They're now written as command handlers:

command-design-pattern-together-with-state-pattern-with-indicators

Command handlers define their logic separately and become registered. Once they're registered, they are "put on hold" until they are called by the dispatch function.

Ultimately, the code functionality stays the same and only the behavior was changed.

Who uses this approach?

One example that immediately pops up in my mind is the lexical package by Facebook here. Lexical is an "extensible JavaScript web text-editor framework with an emphasis on reliability, accessibility, and performance".

In lexical, commands for the editor can be registered and become available for use. The handling logic is defined when they get registered so they can be identified for the dispatch call.

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!

💖 💪 🙅 🚩
jsmanifest
jsmanifest

Posted on November 24, 2022

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

Sign up to receive the latest update from our blog.

Related