Introduction to The Command Design Pattern
The Command design pattern is a behavioral design pattern that turns a request into a standalone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request's execution, and support undoable operations.
Command: This is the
ICommand
interface in the provided code. This interface declares a method for a particular action.Concrete Command:
TurnOnCommand
andTurnOffCommand
are the Concrete Commands in the provided code. They implement theICommand
interface and execute an action on the receiver (Light
).Client: The
Client
class in your code is the Client. It creates and configures the Concrete Command objects and the Receiver.Invoker:
SimpleRemoteControl
is the Invoker. It's responsible for initiating the request by callingexecute
on the command.Receiver:
Light
is the Receiver in your code. It's the object that knows how to perform the actual work when the execute method is called on a command.
In essence, a Command pattern encapsulates a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.
Important Components Of The Command Pattern
The Command pattern consists of four parts:
- Command declares an interface for executing an operation.
- Concrete Command defines a binding between a Receiver object and an action. Implements Execute by invoking the corresponding operation(s) on Receiver.
- Client creates a ConcreteCommand object and sets its receiver.
- Invoker asks the command to carry out the request.
- Receiver knows how to perform the operations associated with carrying out a request. Any class may serve as a Receiver.
Classic Implementation Of The Command Pattern
First, we start with the Command interface.
interface ICommand {
execute(): void;
undo(): void;
}
Next, we have a Receiver. The receiver is an object that performs a set of actions when a command is executed. In this case, our receiver is a Light object that can be turned on and off.
class Light {
public turnOn(): void {
console.log("The light is on");
}
public turnOff(): void {
console.log("The light is off");
}
}
Then we have our Concrete Commands. These classes implement the Command interface and bind the actions of the Receiver to the command.
class TurnOnCommand implements ICommand {
private light: Light;
constructor(light: Light) {
this.light = light;
}
public execute(): void {
this.light.turnOn();
}
public undo(): void {
this.light.turnOff();
}
}
class TurnOffCommand implements ICommand {
private light: Light;
constructor(light: Light) {
this.light = light;
}
public execute(): void {
this.light.turnOff();
}
public undo(): void {
this.light.turnOn();
}
}
The Invoker is responsible for executing a command. In this case, our invoker is a SimpleRemoteControl that can accept a command and execute it.
class SimpleRemoteControl {
private currentCommand!: ICommand;
private undoCommand!: ICommand;
private commandQueue: ICommand[] = [];
public setCommand(command: ICommand): void {
this.undoCommand = this.currentCommand;
this.currentCommand = command;
this.commandQueue.push(command);
}
public buttonWasPressed(): void {
if (this.commandQueue.length) {
const command = this.commandQueue.shift();
command?.execute();
}
}
public undoButtonWasPressed(): void {
this.undoCommand.undo();
}
public hasCommands(): boolean {
return this.commandQueue.length > 0;
}
}
Finally, we have our Client, which sets up the associations between the Invoker and the Commands.
//Client Code
const remote: SimpleRemoteControl = new SimpleRemoteControl();
const light: Light = new Light();
// Turning on the light
remote.setCommand(new TurnOnCommand(light));
remote.buttonWasPressed();
// Turning off the light
remote.setCommand(new TurnOffCommand(light));
remote.buttonWasPressed();
// Undo last operation (turn the light back on)
remote.undoButtonWasPressed();
// Processing remaining commands in the queue (if any)
while (remote.hasCommands()) {
remote.buttonWasPressed();
}
The Command pattern encapsulates a request as an object, thereby allowing clients to be parameterized with queues, requests, and operations. It's very useful when you need to issue requests to objects without knowing anything about the operation being requested or the receiver of the request.
How Above Implementation Jusifies The DefinitionAbsolutely.
"Pass requests as a method arguments": In our revised code, we continue to pass a command object (
TurnOnCommand
orTurnOffCommand
) as an argument to thesetCommand
method of ourSimpleRemoteControl
class. Each command object encapsulates a specific request, which is executed whenbuttonWasPressed
method is invoked."Delay or queue a request's execution": The
setCommand
method pushes the commands into thecommandQueue
array within theSimpleRemoteControl
class, and these commands are only executed when thebuttonWasPressed
method is called. Thus, we can queue multiple commands and they will be executed in the order they were added (FIFO), effectively delaying their execution until required."Support undoable operations": With the
undo
method in ourICommand
interface and its concrete implementations inTurnOnCommand
andTurnOffCommand
, we now support undoable operations. Whenever a command is executed, thesetCommand
method saves the command asundoCommand
. When theundoButtonWasPressed
method is called on theSimpleRemoteControl
instance, it calls theundo
method on the most recently executed command, effectively reversing the operation of that command.
When To Use The Command Pattern
The Command design pattern is an appropriate solution in several scenarios. A few "code smells" or indicators that the Command pattern may be appropriate include:
Complex commands
If an operation involves calling different methods on
various objects and/or arranging these calls in a particular sequence,
encapsulating such a complex operation into a command object can simplify the
code.
The Command pattern encapsulates complex operations into a
single object. For example, the TurnOnCommand
and TurnOffCommand
in our
code encapsulate the actions of turning the light on and off. While these
commands are relatively simple, they could be expanded to include more
complex sequences of method calls.
class TurnOnCommand implements ICommand {
private light: Light;
constructor(light: Light) {
this.light = light;
}
public execute(): void {
this.light.turnOn();
}
//...
}
Parameterizing objects with operations
If you have an object that needs
to perform some operation, but you want to specify the exact operation at
runtime, the Command pattern can be useful. The object can be parameterized
with a Command that represents the desired operation.
In our code, SimpleRemoteControl
is
parameterized with a command that can be executed later. The command is set
by calling the setCommand
method and executed by
calling buttonWasPressed
.
class SimpleRemoteControl {
private currentCommand!: ICommand;
//...
public setCommand(command: ICommand): void {
//...
this.currentCommand = command;
}
public buttonWasPressed(): void {
//...
this.currentCommand.execute();
}
}
Operations that need to be performed at a later time or in a different context
If you need to execute an operation but you cannot do so
immediately, you can use the Command pattern to create a command object that
represents the operation and execute it later.
The command queue in SimpleRemoteControl
allows commands to be
delayed and executed later.
class SimpleRemoteControl {
private commandQueue: ICommand[] = [];
public setCommand(command: ICommand): void {
//...
this.commandQueue.push(command);
}
public buttonWasPressed(): void {
if (this.commandQueue.length) {
const command = this.commandQueue.shift();
command?.execute();
}
}
//...
}
Supporting undo/redo
If your application needs to support undoable
operations, the Command pattern is a good choice. Each action can be
represented as a command, and the history of commands can be stored to
support undo and redo.
The undo
method in our command classes
and undoButtonWasPressed
method in SimpleRemoteControl
provide support
for undoing commands.
class SimpleRemoteControl {
private undoCommand!: ICommand;
public setCommand(command: ICommand): void {
this.undoCommand = this.currentCommand;
//...
}
public undoButtonWasPressed(): void {
this.undoCommand.undo();
}
//...
}
class TurnOnCommand implements ICommand {
public undo(): void {
this.light.turnOff();
}
//...
}
Implementing a job queue
If your application needs to support a queue of
tasks that will be executed at different times or by different threads, the
Command pattern can be useful. Each task can be represented as a command and
placed in a queue. Workers can execute commands from the queue as they are
available.
The commandQueue
in SimpleRemoteControl
represents a job queue, where each job is a command that can be executed by
calling buttonWasPressed
.
class SimpleRemoteControl {
private commandQueue: ICommand[] = [];
public setCommand(command: ICommand): void {
this.commandQueue.push(command);
}
public buttonWasPressed(): void {
if (this.commandQueue.length) {
const command = this.commandQueue.shift();
command?.execute();
}
}
//...
}
Supporting transactional behavior
If your application needs to support operations that can be grouped and executed as a transaction, the Command pattern can be useful. Commands can be grouped and either all successfully executed, or all rolled back in case of error.
While our example doesn't explicitly
demonstrate this, the Command pattern can be used to support transactions.
For example, a command could represent a database operation, and a series of
such commands could be executed as a transaction. If one command fails,
the undo
methods could be called on all the previously executed commands to
roll back the transaction.
Sure, here's how the Command pattern and the provided TypeScript code apply to these situations:
Remember, using the Command pattern comes with trade-offs. While it provides flexibility, it can also introduce complexity and could be overkill for simple scenarios. Always consider whether the pattern's benefits are worth the potential increase in complexity for your particular use case.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSWhat Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.