Skip to main content

Command

The Command pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you parameterize methods with different requests, delay or queue a request's execution, and support undoable operations.

OOP Style​

In the Object-Oriented Programming (OOP) style, the Command pattern involves the following components:

  1. Command Interface: An interface with a method for executing the command.
  2. Concrete Command: Implements the Command interface and defines the binding between a Receiver object and an action. It doesn't perform the action itself; it just knows who should perform it (the receiver) and what action to perform.
  3. Receiver: The object that performs the actual action. This is the object that has the knowledge of how to perform the actions needed by the command. When a command's execute() method is called, it will in turn call the appropriate method(s) on the receiver to perform the actual work.
  4. Invoker/Caller: Sends the command to execute. This is what triggers the command. It holds a command and at some point asks the command to carry out a request by calling its execute() method.
  5. Client: Creates a ConcreteCommand and sets its receiver.
command

Command pattern in the Wikipedia

interface and concrete command
interface Command {
execute(): void;
undo(): void;
log(): void;
}

class ToggleLightCommand implements Command {
private isOn: boolean = false;

constructor(private light: Light) {}

execute(): void {
if (this.isOn) {
this.light.turnOff();
this.isOn = false;
} else {
this.light.turnOn();
this.isOn = true;
}
}

undo(): void {
this.execute(); // Toggle the light to its previous state
}

log(): void {
console.log("Toggling the light");
}
}
receiver
class Light {
turnOn(): void {
console.log("Light turned on");
}

turnOff(): void {
console.log("Light turned off");
}
}
Invoker/Caller
class RemoteControl {
submit(command: Command): void {
command.log();
command.execute();
}
}
Client
const light = new Light();
const toggleLight = new ToggleLightCommand(light);
const remote = new RemoteControl();

remote.submit(toggleLight); // Turns the light on
remote.submit(toggleLight); // Turns the light off

FP Style​

In Functional Programming, commands can be represented as pure functions. You won't have classes and instances. Instead, you can use higher-order functions and closures to encapsulate state and behavior.

type Command = {
execute: () => void;
undo: () => void;
log: () => void;
};

type Light = { turnOn: () => void; turnOff: () => void };

function createToggleLightCommand(light: Light): Command {
let isOn = false;
return {
execute: () => {
if (isOn) {
light.turnOff();
} else {
light.turnOn();
}
isOn = !isOn;
},
undo: () => {
// Call execute to toggle the light's state
this.execute();
},
log: () => {
console.log("Toggling the light");
},
};
}

const light = {
turnOn: () => console.log("Light turned on"),
turnOff: () => console.log("Light turned off"),
};

const toggleLight = createToggleLightCommand(light);

toggleLight.log();
toggleLight.execute(); // Turns the light on
toggleLight.undo(); // Turns the light off

In this FP style example, createToggleLightCommand is a factory function that returns an object with execute, undo, and log methods. The state (isOn) is enclosed within the scope of the factory function, providing encapsulation without the need for a class.

Command Pattern State Management​

info

Commands focus on the 'what' (the action to be performed), while state management handles the 'how' (how the application's state changes in response).

In the Command pattern, the state management can vary depending on the specific implementation and the complexity of the application. Generally, the state can be managed by one or more of the following components:

Command Objects: Each command object can maintain its own state. This is particularly useful for state that is specific to the command's operation, like parameters needed for execution or undo information.

Invoker: The invoker is responsible for initiating the commands. In some designs, the invoker also keeps track of command history, which is essential for features like undo/redo functionality. This can be seen as a form of state management, particularly regarding the sequence and state of command execution.

Receiver: The receiver is the object that actually performs the work when the command is executed. It often holds the state that the commands act upon. For instance, if you have a command that modifies a document, the document itself (the receiver) would maintain its state.

Client: The client creates the command objects and associates them with the appropriate receivers. In some cases, especially in simpler implementations, the client might hold onto state that is used to create or configure commands.

External State Storage: In more complex systems, especially those following principles like Clean Architecture or CQRS (Command Query Responsibility Segregation), the state might be managed externally in a database or a different service altogether. This allows for more robust state management and can facilitate things like event sourcing.

The choice of where to keep the state depends on factors like the complexity of the operations, the need for undo/redo functionality, and the overall architecture of the application.

info

In a typical Command pattern setup, the emphasis is on decoupling the command execution from the invoker and receiver, which can lead to various ways of managing the state effectively.

Relationship between Command Pattern and Redux Pattern​

  1. Encapsulation of Actions: In the Command pattern, actions are encapsulated in objects. Similarly, in Redux, actions are objects that describe what happened, but don't describe how the application's state changes.

  2. State Management: Both patterns are concerned with managing state. In Redux, the state of the application is stored in a single immutable store. In the Command pattern, the state can be altered by executing commands which may also support undo operations.

  3. Undo/Redo: The Command pattern naturally supports undo/redo functionality by keeping a history of executed commands and their reverse operations. Redux can also support undo/redo by keeping a history of states, often implemented using middleware.

  4. Unidirectional Data Flow: Redux follows a strict unidirectional data flow, which is conceptually similar to how commands are dispatched and handled in the Command pattern.

Sequential Commands with Undo Functionality​

Let's create an example where a series of commands are executed sequentially on an object, and then we undo all these commands. We'll use a simple scenario of a text editor where commands can add or remove text.

Receiver: Text Editor
class TextEditor {
private content: string = "";

append(text: string): void {
this.content += text;
}

delete(count: number): void {
this.content = this.content.substring(0, this.content.length - count);
}

getContent(): string {
return this.content;
}
}
Concrete Commands
class AppendCommand implements Command {
constructor(private editor: TextEditor, private text: string) {}

execute(): void {
this.editor.append(this.text);
}

undo(): void {
this.editor.delete(this.text.length);
}
}

class DeleteCommand implements Command {
private deletedText: string = "";

constructor(private editor: TextEditor, private count: number) {}

execute(): void {
this.deletedText = this.editor.getContent().slice(-this.count);
this.editor.delete(this.count);
}

undo(): void {
this.editor.append(this.deletedText);
}
}
Client
const editor = new TextEditor();
const commands: Command[] = [
new AppendCommand(editor, "Hello"),
new AppendCommand(editor, " World"),
new DeleteCommand(editor, 6),
];

// Execute all commands
commands.forEach((cmd) => cmd.execute());
console.log("Content after commands:", editor.getContent());

// Undo all commands
commands.reverse().forEach((cmd) => cmd.undo());
console.log("Content after undoing:", editor.getContent());

In this example, we have two types of commands: AppendCommand and DeleteCommand, both implementing the Command interface. The TextEditor class is the receiver that performs the actual text manipulation. We execute a series of commands on the TextEditor instance and then undo them in reverse order.

Active Object Pattern​

warning

They serve different purposes but can be used together effectively.

  • Purpose: The Active Object Pattern decouples method execution from method invocation to enhance concurrency and simplify synchronized access to objects that reside in their own threads of control. It's particularly useful in scenarios where operations need to be executed asynchronously.
  • Key Components: This pattern involves a Proxy, a Method Request, a Scheduler, and a Servant. The Proxy receives the client's request and translates it into a Method Request, which is then queued by the Scheduler. The Servant is the object that actually performs the operation.

Their Relationship​

  1. Decoupling: Both patterns emphasize decoupling. The Command Pattern decouples the sender of a request from its executor, while the Active Object Pattern decouples the execution of a method from the calling thread.

  2. Asynchronous Processing: The Active Object Pattern can be used to make the execution of commands asynchronous. Commands can be turned into Method Requests which are then scheduled and executed by the Active Object's mechanism.

  3. Enhancing Flexibility and Scalability: When combined, these patterns can offer a flexible and scalable architecture. For instance, in a web application, UI actions can be encapsulated as Command objects, which are then processed asynchronously by an Active Object, enhancing responsiveness and efficiency.

  4. Use in Web Development: In the context of web development with technologies like React, TypeScript, and Redux, the Command Pattern can be used to encapsulate user actions or Redux actions, while the Active Object Pattern can manage asynchronous tasks like API calls or complex computations, keeping the UI responsive.

  5. Complex Operations Management: For complex operations that require both undo/redo functionality and asynchronous execution, combining these patterns can be particularly effective.

In summary, while the Command Pattern and the Active Object Pattern serve different purposes—one focusing on command encapsulation and the other on asynchronous method execution—they can be combined to create a robust and efficient system. This combination is particularly powerful in web development scenarios that require both decoupling of execution logic and efficient handling of asynchronous operations.