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:
- The initiator (also called the invoker)
- 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'
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:
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 |
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),
}))
}
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'
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:
- Create the
commands
variable. This will store registered commands and their callback handlers. - Define
registerCommand
. This will register new commands and their callback handlers to thecommands
object. - 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))
}
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),
}))
})
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:
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:
The functions notifySubscribers
, addProfile
, and setBackgroundColor
are gone, but all of their logic still remains. They're now written as command handlers:
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!
Top comments (4)
probably need an unsubscribe for good memory management
These design pattern posts are very very valuable. Especially given that most design pattern guides you will see on the web will be written as a class instead of a function.
Definitely on the look out for more design patterns written in a functional programming approach from you.
Thank you
For me command pattern is same as event driven.