Command Pattern in JavaScript: A Complete Guide
This article provides a comprehensive overview of the Command design pattern and demonstrates how to structure and implement it using JavaScript. You will learn the core concepts behind the pattern, examine its primary components—the Command, Receiver, Invoker, and Client—and see a practical code implementation featuring execute and undo operations.
What is the Command Pattern?
The Command Pattern is a behavioral design pattern that encapsulates a request as an object. By converting a request into a standalone object containing all the details about the operation, the pattern decouples the object that invokes the operation from the object that actually knows how to perform it.
This separation of concerns makes it easy to pass operations as method arguments, queue or log operations, schedule tasks, and implement features like undo and redo.
Key Components of the Command Pattern
The Command pattern relies on four main components:
- Receiver: The object that contains the core business logic and performs the actual work.
- Command: An object or interface that defines the
execution method (commonly
execute()and optionallyundo()). - Concrete Command: Implements the Command interface and binds an action to the Receiver by calling the corresponding methods on the Receiver.
- Invoker: The object that holds the command and triggers its execution (e.g., a button, a scheduler, or a command manager).
- Client: The code that configures the application by creating receivers, instantiating commands, and assigning them to invokers.
Structuring the Command Pattern in JavaScript
In modern JavaScript, the Command pattern can be structured using ES6 classes. Below is a step-by-step implementation of a simple calculator that supports both executing commands and undoing them.
1. The Receiver
The Receiver performs the actual computation.
class Calculator {
constructor() {
this.value = 0;
}
add(value) {
this.value += value;
console.log(`Current Value: ${this.value}`);
}
subtract(value) {
this.value -= value;
console.log(`Current Value: ${this.value}`);
}
}2. The Concrete Commands
Concrete commands encapsulate the action and hold a reference to the receiver.
class AddCommand {
constructor(calculator, valueToAdd) {
this.calculator = calculator;
this.valueToAdd = valueToAdd;
}
execute() {
this.calculator.add(this.valueToAdd);
}
undo() {
this.calculator.subtract(this.valueToAdd);
}
}
class SubtractCommand {
constructor(calculator, valueToSubtract) {
this.calculator = calculator;
this.valueToSubtract = valueToSubtract;
}
execute() {
this.calculator.subtract(this.valueToSubtract);
}
undo() {
this.calculator.add(this.valueToSubtract);
}
}3. The Invoker
The Invoker executes the commands and maintains a history to support undo operations.
class CalculatorInvoker {
constructor() {
this.history = [];
}
executeCommand(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
console.log('Undoing last operation...');
command.undo();
} else {
console.log('No operations to undo.');
}
}
}4. The Client Code
The Client ties the components together by instantiating the receiver, the invoker, and executing specific commands.
// Initialize components
const calculator = new Calculator();
const invoker = new CalculatorInvoker();
// Create commands
const addTen = new AddCommand(calculator, 10);
const subtractFive = new SubtractCommand(calculator, 5);
// Execute commands
invoker.executeCommand(addTen); // Current Value: 10
invoker.executeCommand(subtractFive); // Current Value: 5
// Undo operations
invoker.undo(); // Undoing last operation... Current Value: 10
invoker.undo(); // Undoing last operation... Current Value: 0When to Use the Command Pattern
- Undo/Redo Functionality: Storing command objects in a history stack makes reversible actions simple to implement.
- Decoupling UI and Logic: User interface elements (such as buttons or menu items) do not need to know the specific business logic they invoke.
- Queuing and Scheduling: Commands can be serialized, placed in a queue, and executed asynchronously or across network boundaries.
- Extensibility: New commands can be added to an existing system without modifying the invoker or receiver code, adhering to the Open/Closed Principle.