Skip to main content

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 and TurnOffCommand are the Concrete Commands in the provided code. They implement the ICommand 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 calling execute 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:

  1. Command declares an interface for executing an operation.
  2. Concrete Command defines a binding between a Receiver object and an action. Implements Execute by invoking the corresponding operation(s) on Receiver.
  3. Client creates a ConcreteCommand object and sets its receiver.
  4. Invoker asks the command to carry out the request.
  5. 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 or TurnOffCommand) as an argument to the setCommand method of our SimpleRemoteControl class. Each command object encapsulates a specific request, which is executed when buttonWasPressed method is invoked.

  • "Delay or queue a request's execution": The setCommand method pushes the commands into the commandQueue array within the SimpleRemoteControl class, and these commands are only executed when the buttonWasPressed 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 our ICommand interface and its concrete implementations in TurnOnCommand and TurnOffCommand, we now support undoable operations. Whenever a command is executed, the setCommand method saves the command as undoCommand. When the undoButtonWasPressed method is called on the SimpleRemoteControl instance, it calls the undo 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.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

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 DETAILS

What 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.

YouTube @cloudaffle