The Command Design Pattern in JavaScript
jsmanifest
Posted on December 24, 2019
Find me on medium
In JavaScript, one of the most popular design patterns that people like to use is the Command Design Pattern, a pattern that allows developers to separate objects that request something from those that want to call their desired methods.
If this is your first time hearing about the command pattern, hopefully by reading this post you will gain a good understanding of what it is, how it works and why we need them in certain situations.
What is the command design pattern?
Design patterns are usually categorized between three different types of categories, and in this case the command pattern falls into the behavioral one.
The reason why is because its purpose is to encapsulate objects that have the double responsibility of deciding which methods to call and what happens inside.
In a visual perspective, that may look something like:
How it works
So essentially, its duty is to split the communication to separate objects so that they become loosely coupled while still maintaining the end goal.
The participants that are involved in this pattern are commonly referred to as:
Client
The client's responsibility is to create the command object and pass it to the invoker.
Invoker
The invoker receives the command object from the client and its only responsibility is to call (or invoke) a command.
Receiver
Then, the receiver receives the command and looks for a method to call based on the received command.
How it looks like
We've just seen an image of how one or more objects behave in code before being applied with the command pattern. Here is how it would look like with it applied:
One evidently large and complex object can end up becoming easier to manage in the long run because the duties of one object vs another were isolated in their own private world instead of being cluttered together.
Command objects by convention usually define a method with a name like execute
which has the responsibility of invoking a method, which by convention is known as the invoker. The object that holds the methods is commonly known as the "receiver".
Why we need the command pattern
The biggest point of using the command pattern is to split the code that wants to do something from the code that is responsible for handling it. When you feel like your code is handling an operation multiple times in different parts of the code, it may become a good idea to start applying it. Having said that, these command objects give us nice benefits for unique situations like being able to centralize the processing of each action/operation individually. This means that in our earlier example our object only needs one .eat()
command, one .jump()
command, and one .run()
command.
When to use
Some example situations in which you can make great use of the command pattern are:
- Undo / Reset
- Since all processing of each action/operations are centralized by commands, they are often fit for implementing undo/reset for applications.
- You need a command to have a life span independent of the original request.
- Furthermore, if you want to queue, specify and execute requests at different times.
- You need undo/redo operations. The command's execution can be stored for reversing its effects. It is important that the Command class implements the methods undo and redo.
- You need to structure a system around high-level operations built on primitive operations.
Real world example
Now lets pretend we are launching a new frog manager application which is intended to help you record and manage a list of frogs over time as they age.
In this application, we're going to have a Frog
class, instantiating some useful properties and methods to help with that:
// Creates and returns a frog api which can help us track activities of each frog
function createFrog(options) {
const _opts = {
name: options.name,
sex: options.sex,
age: options.age,
}
const foodsEaten = []
const wordsSpoken = []
return {
getOption(key) {
return _opts[key]
},
getFoodsConsumed() {
return foodsEaten
},
getWordsSpoken() {
return wordsSpoken
},
eat(food) {
console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
foodsEaten.push(food)
},
talk(words) {
console.log(words)
wordsSpoken.push(...words)
},
}
}
Great! Now we can make multiple frogs by instantiating them:
const mikeTheFrog = createFrog({ name: 'mike', sex: 'male', age: 1 })
const sallyTheOtherFrog = createFrog({ name: 'sally', sex: 'female', age: 4 })
const michelleTheLastFrog = createFrog({
name: 'michelle',
sex: 'female',
age: 10,
})
Lets pretend move on to making our frog application come to life:
index.js
const api = {
fetchFrogs: function() {
return Promise.resolve([
{ id: 1, name: 'mike', sex: 'male', age: 1 },
{ id: 2, name: 'sally', sex: 'female', age: 2 },
{ id: 3, name: 'michelle', sex: 'female', age: 9 },
])
},
saveToDb: function(frogs) {
// Just pretend this is actually saving to a real database
console.log(`Saving ${frogs.length} frogs to our database...`)
return Promise.resolve()
},
}
async function init() {
try {
const frogs = await api.fetchFrogs()
return frogs.map((data) => createFrog(data))
} catch (error) {
console.error(error)
throw error
}
}
function createFrogsManager() {
const frogs = []
return {
addFrog(frog) {
frogs.push(frog)
return this
},
getFrogs() {
return frogs
},
getMaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('sex') === 'male'
})
},
getFemaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('sex') === 'female'
})
},
feedFrogs(food) {
frogs.forEach((frog) => {
frog.eat(food)
})
return this
},
save: function() {
return Promise.resolve(api.saveToDb(frogs))
},
}
}
function Food(name, type, calories) {
this.name = name
this.type = type
this.calories = calories
}
const fly = new Food('fly', 'insect', 1.5)
const dragonfly = new Food('dragonfly', 'insect', 4)
const mosquito = new Food('mosquito', 'insect', 1.8)
const apple = new Food('apple', 'fruit', 95)
init()
.then((frogs) => {
const frogsManager = createFrogsManager()
// Add each fetched frog to our managing list so we can manage them
frogs.forEach((frog) => {
frogsManager.addFrog(frog)
})
const genders = {
males: frogsManager.getMaleFrogs(),
females: frogsManager.getFemaleFrogs(),
}
// Lets feed the frogs and then save this new data to the database
frogsManager
.feedFrogs(fly)
.feedFrogs(mosquito)
.save()
console.log(
'We reached the end and our database is now updated with new data!',
)
console.log(
`Fed: ${genders.males.length} male frogs and ${genders.females.length} female frogs`,
)
frogsManager.getFrogs().forEach((frog) => {
console.log(
`Frog ${frog.getOption('name')} consumed: ${frog
.getFoodsConsumed()
.map((food) => food.name)
.join(', ')}`,
)
})
})
.catch((error) => {
console.error(error)
})
Result:
Our application is becoming extremely valuable!
Now keep in mind that we did not apply the command design pattern in the code--however the code runs perfectly fine and we can be fine if our frog application wasn't going to grow any bigger.
Now lets take a real close look to our createFrogsManager
api. We can see that this gives us an api to manage a list of frogs over time by providing convenient utilities to track the activities of multiple frogs.
However, if you look closely there are some potential issues that can bite us in the future.
The first thing we see is that our api createFrogsManager
is tightly coupled with carrying out the methods that we want to work with. Our code at the end utilizes this interface and directly invokes its methods, being entirely dependent on the returned api. This api is responsible for both invoking and handling each operation.
For example, lets talk about these two methods returned for us to use:
getMaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('sex') === 'male'
})
},
getFemaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('sex') === 'female'
})
}
What if in the future the path to get each frog's gender was slightly changed?
So instead of this:
function createFrog(options) {
const _opts = {
name: options.name,
sex: options.sex,
age: options.age,
}
const foodsEaten = []
const wordsSpoken = []
return {
getOption(key) {
return _opts[key]
},
getFoodsConsumed() {
return foodsEaten
},
getWordsSpoken() {
return wordsSpoken
},
eat(food) {
console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
foodsEaten.push(food)
},
talk(words) {
console.log(words)
wordsSpoken.push(...words)
},
}
}
It became this instead:
function createFrog(options) {
const _opts = {
name: options.name,
gender: options.gender,
age: options.age,
}
const foodsEaten = []
const wordsSpoken = []
return {
getOption(key) {
return _opts[key]
},
getFoodsEaten() {
return foodsEaten
},
getWordsSpoken() {
return wordsSpoken
},
eat(food) {
console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
foodsEaten.push(food)
},
talk(words) {
console.log(words)
wordsSpoken.push(...words)
},
}
}
Days have passed and things have been silent. No reports of complaints so everything must be fine. After all, our server has been up and running 24/7 and users have been using our application since then.
Then, a customer had called our customer service department 2 weeks later and reported that all of her frogs died and blamed our platform for her loss after placing her entire trust into us believing that our intelligent algorithms would help her make the right decisions for keeping them properly managed.
Our developers were immediately notified and were asked to debug the situation to see if there were any glitches in code that might have sparked this horrifying event.
At closer examination, we run a test code and realized that our code is actually reporting incorrect information!
What?! No way!
One of the developers pointed out that the problem was that the .sex
key of a frog object was renamed to .gender
!
const _opts = {
name: options.name,
gender: options.gender,
age: options.age,
}
We had to go find and change the code that used the previous references by key so that it works normally again:
getMaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('gender') === 'male'
})
},
getFemaleFrogs() {
return frogs.filter((frog) => {
return frog.getOption('gender') === 'female'
})
}
Oh, and if you haven't caught it yet, there was another issue with our code. It seems the method getFoodsConsumed
inside createFrog
was also changed to getFoodsEaten
:
Previous:
getFoodsConsumed() {
return foodsEaten
}
Current:
getFoodsEaten() {
return foodsEaten
}
In another scenario, what if the createFrogsManager
api had some of its methods renamed, like .save
to .saveFrogs
or .getFrogs
to .getAllFrogs
? This means that every single part of our code that used these methods manually need to be updated to the new names!
So a major problem we're having here in the examples is that we're having to go fix all of our code that were affected to the change! It becomes a hide and seek game. But it doesn't need to be.
So how can the command pattern help turn this around?
In the beginning of this post we mentioned that the command pattern allows developers to separate objects that request something away from those that want to call their desired methods.
Also somewhere in the beginning of this post we mentioned the three participants that will be involved. They were the client, invoker and receiver.
Here is a representation of that:
Lets refactor our createFrogsManager
using the command approach:
function createFrogsManager() {
const frogs = []
return {
execute(command, ...args) {
return command.execute(frogs, ...args)
},
}
}
This is all we really need because we're going to let the commands do the work.
We'll go ahead and create the Command
constructor that we will use to create the concrete commands for each method of the api:
function Command(execute) {
this.execute = execute
}
Now that that's settled, lets go ahead and make the concrete commands:
function AddFrogCommand(frog) {
return new Command(function(frogs) {
frogs.push(frog)
})
}
function GetFrogsCommand() {
return new Command(function(frogs) {
return frogs
})
}
function FeedFrogsCommand(food) {
return new Command(function(frogs) {
frogs.forEach((frog) => {
frog.eat(food)
})
})
}
function SaveCommand() {
return new Command(function(frogs) {
api.saveToDb(
frogs.map((frog) => ({
name: frog.name,
gender: frog.gender,
age: frog.age,
})),
)
})
}
With this in place, we can use it like so:
function Food(name, type, calories) {
this.name = name
this.type = type
this.calories = calories
}
const mikeTheFrog = createFrog({
name: 'mike',
gender: 'male',
age: 2,
})
const sallyTheFrog = createFrog({
name: 'sally',
gender: 'female',
age: 1,
})
const frogsManager = createFrogsManager()
frogsManager.execute(new AddFrogCommand(mikeTheFrog))
frogsManager.execute(new FeedFrogsCommand(new Food('apple', 'fruit', 95)))
frogsManager.execute(new FeedFrogsCommand(new Food('fly', 'insect', 1)))
frogsManager.execute(new AddFrogCommand(sallyTheFrog))
frogsManager.execute(new SaveCommand())
const updatedFrogs = frogsManager.execute(new GetFrogsCommand())
Result:
I'd like to mention that in the visual, the receiver is blank because in JavaScript all functions and objects are basically commands themselves, which we demonstrated in the .execute
by invoking commands directly:
function createFrogsManager() {
const frogs = []
return {
execute(command, ...args) {
return command.execute(frogs, ...args)
},
}
}
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!
Find me on medium
Posted on December 24, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 27, 2024
October 21, 2024