Command Pattern in TypeScript
The Command Pattern is a behavioral design pattern that turns requests or operations into standalone objects containing all the information needed to execute the action later. It decouples the sender (who initiates the request) from the receiver (who performs the action), enabling more flexible and maintainable code.
In TypeScript, this pattern is particularly useful for:
- Implementing undo/redo functionality.
- Building task queues or macros.
- Managing UI interactions (e.g., button clicks triggering complex logic).
Source: Wikimedia Commons
🔹 Why It Matters
- Decoupling: Separates the requester from the executor.
- Extensibility: Easily add new commands without modifying existing code.
- Undo/Redo: Track and reverse actions by storing command history.
- Transactional Behavior: Group commands into atomic operations.
🔹 Core Concepts
The Command Pattern involves four key components:
- Command Interface: Declares an
execute()
method. - Concrete Command: Implements the interface and invokes the receiver’s action.
- Receiver: Performs the actual work.
- Invoker: Request the command execution.
Example Scenario: Light Control System
Imagine a smart home app controlling lights. The Invoker (remote control) triggers Commands (turn on/off), which delegate to the Receiver (light bulb).
🔹 Code Walkthrough
Let’s implement a simple light control system in TypeScript.
1. Define the Command Interface
// Command Interface
interface Command {
execute(): void;
undo(): void; // Optional for undo functionality
}
2. Create Concrete Commands
// Receiver (Light Bulb)
class Light {
turnOn(): void {
console.log("Light is ON");
}
turnOff(): void {
console.log("Light is OFF");
}
}
// Concrete Command: TurnOnLight
class TurnOnLightCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
// Concrete Command: TurnOffLight
class TurnOffLightCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.turnOff();
}
undo(): void {
this.light.turnOn();
}
}
3. Implement the Invoker (Remote Control)
// Invoker (Remote Control)
class RemoteControl {
private commandHistory: Command[] = [];
pressButton(command: Command): void {
command.execute();
this.commandHistory.push(command); // Track for undo
}
undoLastCommand(): void {
if (this.commandHistory.length > 0) {
const lastCommand = this.commandHistory.pop();
lastCommand?.undo();
}
}
}
4. Test the System
// Usage
const light = new Light();
const turnOnCommand = new TurnOnLightCommand(light);
const turnOffCommand = new TurnOffLightCommand(light);
const remote = new RemoteControl();
remote.pressButton(turnOnCommand); // Light is ON
remote.pressButton(turnOffCommand); // Light is OFF
remote.undoLastCommand(); // Light is ON (undo)
🔹 Common Mistakes
- Over-Engineering: Avoid using the Command Pattern for simple actions (e.g., direct method calls suffice).
- Memory Leaks: Forget to clear the command history in the Invoker.
- Tight Coupling: If the Command directly knows too much about the Invoker or Receiver, rethink the design.
🔹 Best Practices
- Keep Commands Stateless: Store only necessary data in the command object.
- Use Interfaces: Define clear contracts for commands and receivers.
- Combine with Other Patterns: Pair with Memento for robust undo/redo (Memento Pattern Guide).
🔹 Final Thoughts
The Command Pattern is a powerful tool for decoupling systems and enabling features like undo/redo and macros. In TypeScript, its type safety enhances reliability, making it ideal for complex applications.
For further reading:
- Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four)
- TypeScript Documentation
- Command Pattern on Refactoring Guru
This article balances theory and practice, making it accessible to beginners while providing depth for intermediate developers. Let me know if you’d like to expand on any section! 🚀