Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

Combining the Command Pattern with State Pattern in JavaScript

JavaScript is a well-liked language identified for its flexibility. It is because of this that makes patterns just like the command sample simpler to implement in our apps.

When there is a design sample on the market that goes effectively hand-in-hand with the state sample, it is arguably the command sample.

In the event you’ve learn considered one of my earlier weblog posts concerning the state sample you would possibly keep in mind this sentence: “The State Sample ensures an object to behave in a predictable, coordinated approach relying on the present “state” of the appliance.”

Within the command sample the principle aim is to separate the communication between two essential members:

  1. The initiator (additionally referred to as the invoker)
  2. The handler

We’re going to incorporate the command sample along with the state sample on this put up. In the event you’re studying both of the 2, my finest recommendation to you as a way to get probably the most out of this put up is to be sure you perceive the stream of the state sample earlier than persevering with to the command sample implementation to raised really feel how the habits of the code drastically modifications whereas the performance stays in tact.

Let’s begin off with an instance utilizing the state sample in order that we will see this in a clearer perspective:

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

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

perform setBackgroundColor(shade) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: shade,
  }))
}

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

  notifySubscribers(prevState, state)
}

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

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

subscribe(
  (perform () {
    perform getColor(size) {
      if (size >= 5 && size <= 8) return 'blue' // Common
      if (size > 8 && size < 10) return 'orange' // Reaching restrict
      if (size > 10) return 'crimson' // Restrict reached
      return 'white' // Default
    }

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

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

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

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

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

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

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

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

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

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

Exit fullscreen mode

Within the instance above we’ve a state and subscribers object. The subscribers object holds a group of callback capabilities. These callback capabilities are referred to as every time the setState perform is known as:

Each time the state updates, all the registered callbacks are referred to as with the earlier state (prevState) and the brand new state (newState) in arguments.

We registered one callback listener in order that we will watch updates to the state and replace the background shade every time the quantity of profiles hits a sure size. The desk beneath exhibits a clearer image lining up the profile counts with the related shade:

Minimal Threshold Background Shade
0 white
5 blue
9 orange
10 crimson

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

So how can the command sample match into this? Effectively, if we glance again into our code we will see that we outlined some capabilities answerable for each calling and dealing with this logic:

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

perform setBackgroundColor(shade) {
  setState((prevState) => ({
    ...prevState,
    backgroundColor: shade,
  }))
}

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

Exit fullscreen mode

We are able to summary these into instructions as an alternative. Within the upcoming code instance it’s going to present the identical code with the command sample carried out in concord with the state sample. After we try this there are solely 2 capabilities left untouched: setState and subscribe.

Let’s go forward and introduce the command sample and commandify our abstracted capabilities:

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

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

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

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

perform registerCommand(title, callback) {
  if (instructions[name]) instructions[name].push(callback)
  else instructions[name] = [callback]
}

perform dispatch(title, motion) {
  instructions[name]?.forEach?.((fn) => fn?.(motion))
}

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

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

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

subscribe(
  (perform () {
    perform getColor(size) {
      if (size >= 5 && size <= 8) return 'blue' // Common
      if (size > 8 && size < 10) return 'orange' // Reaching restrict
      if (size > 10) return 'crimson' // Restrict reached
      return 'white' // Default
    }

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

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

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

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

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

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

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

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

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

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

Exit fullscreen mode

It is a lot clearer now figuring out which capabilities we have to replace the state. We are able to separate the whole lot else into their very own separate command handlers. That approach we will isolate them to be in their very own separate file or location so we will work with them simpler.

Right here had been the steps demonstrated in our up to date instance to get to that time:

  1. Create the instructions variable. It will retailer registered instructions and their callback handlers.
  2. Outline registerCommand. It will register new instructions and their callback handlers to the instructions object.
  3. Outline dispatch. That is answerable for calling the callback handlers related to their command.

With these three steps we completely arrange our instructions to be registered by the consumer code, permitting them to implement their very own instructions and logic. Discover how how our registerCommand and dispatch capabilities do not want to concentrate on something associated to our state objects.

We are able to simply benefit from that and proceed with isolating them right into a separate file:

instructions.js

// Personal object hidden inside this scope
let instructions = {}

export perform registerCommand(title, callback) {
  if (instructions[name]) instructions[name].push(callback)
  else instructions[name] = [callback]
}

export perform dispatch(title, motion) {
  instructions[name]?.forEach?.((fn) => fn?.(motion))
}
Enter fullscreen mode

Exit fullscreen mode

As for the precise logic written in these traces:

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

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

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

Exit fullscreen mode

Usually this might all be left to the consumer code to resolve that (technically, our final code snippet are representing the consumer code). Some library authors additionally outline their very own command handlers for inner use however the identical idea nonetheless applies. One observe I see typically is having their inner logic put right into a separate file with its file title prefixed with "inner" (instance: internalCommands.ts). It is price noting that 99% of the time the capabilities in these recordsdata are by no means exported out to the person. That is why they’re marked inner.

The picture beneath is a diagram of what our code seemed like earlier than implementing the command design sample to it:

state-pattern-before-command-pattern-integration

The bubbles within the purple symbolize capabilities. The 2 capabilities setBackgroundColor and addProfile are included amongst them. Particularly for these two nevertheless name setState on to facilitate modifications to state. In different phrases they each name and deal with the state replace logic to particular slices of the state they’re occupied with.

Now take a look on the diagram beneath. This picture exhibits how our code seems like after implementing the sample:

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

The capabilities notifySubscribers, addProfile, and setBackgroundColor are gone, however all of their logic nonetheless stays. They’re now written as command handlers:

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

Command handlers outline their logic individually and change into registered. As soon as they’re registered, they’re “placed on maintain” till they’re referred to as by the dispatch perform.

In the end, the code performance stays the identical and solely the habits was modified.



Who makes use of this method?

One instance that instantly pops up in my thoughts is the lexical package deal by Fb here. Lexical is an “extensible JavaScript internet text-editor framework with an emphasis on reliability, accessibility, and efficiency”.

In lexical, instructions for the editor may be registered and change into accessible to be used. The dealing with logic is outlined once they get registered to allow them to be recognized for the dispatch name.



Conclusion

And that concludes the tip of this put up! I hope you discovered this to be useful and look out for extra sooner or later!

Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?