Command Pattern: Encapsulating Actions as Objects
The Command pattern turns a request into a stand-alone object that contains all information about the request. Learn how this enables undo, queuing, and logging.
Imagine you are building a text editor. You need to implement operations like “copy,” “paste,” and “delete.” You also need to support undoing these operations. How do you design this in a clean, extensible way?
The Command pattern provides a solution. It encapsulates a request (an action) as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
The Core Idea: Actions as Objects
Instead of calling a method directly on a receiver object, you create a “Command” object that holds all the necessary information to perform the action. This includes:
- The action to be performed (e.g., a method reference).
- The object that will perform the action (the “receiver”).
- Any parameters needed for the action.
This command object has a simple, common interface, typically a single execute() method.
Example: A Simple Text Editor
Let’s model a basic text editor that can add and delete text.
The Receiver
This is the object that does the actual work.
// The Receiver: Performs the actual operations
class TextEditor {
private content: string = "";
addText(text: string): void {
this.content += text;
console.log(`Current content: "${this.content}"`);
}
deleteText(chars: number): void {
this.content = this.content.slice(0, -chars);
console.log(`Current content: "${this.content}"`);
}
}
The Command Interface
All concrete commands will implement this interface. It often includes an undo() method for undoable operations.
interface Command {
execute(): void;
undo(): void;
}
Concrete Commands
These classes bind a receiver with a specific action.
class AddTextCommand implements Command {
constructor(
private editor: TextEditor,
private textToAdd: string
) {}
execute(): void {
this.editor.addText(this.textToAdd);
}
undo(): void {
this.editor.deleteText(this.textToAdd.length);
}
}
class DeleteTextCommand implements Command {
private deletedText: string = ""; // We need to store this for undo
constructor(
private editor: TextEditor,
private charsToDelete: number
) {}
execute(): void {
// Before deleting, we need to know what text was deleted
// In a real app, you'd get this from the editor
this.deletedText = " ... some text ... "; // Simplified for example
this.editor.deleteText(this.charsToDelete);
}
undo(): void {
this.editor.addText(this.deletedText); // Not perfectly accurate, but shows the concept
}
}
The Invoker
The invoker is responsible for executing commands and, crucially, can keep a history of them to manage undo/redo.
class EditorInvoker {
private history: Command[] = [];
executeCommand(command: Command): void {
command.execute();
this.history.push(command);
}
undoLastCommand(): void {
const lastCommand = this.history.pop();
if (lastCommand) {
lastCommand.undo();
}
}
}
// Putting it all together
const editor = new TextEditor();
const invoker = new EditorInvoker();
const addHello = new AddTextCommand(editor, "Hello ");
const addWorld = new AddTextCommand(editor, "World!");
invoker.executeCommand(addHello); // "Hello "
invoker.executeCommand(addWorld); // "Hello World!"
invoker.undoLastCommand(); // "Hello "
invoker.undoLastCommand(); // ""
Python Example: A Smart Home Remote Control
In Python, we can use callable objects or classes to implement the same pattern. Let’s create a remote control for smart home devices.
from abc import ABC, abstractmethod
# The Receiver classes
class Light:
def turn_on(self):
print("Light is ON")
def turn_off(self):
print("Light is OFF")
class Fan:
def start_rotating(self):
print("Fan is ON")
def stop_rotating(self):
print("Fan is OFF")
# The Command interface and Concrete Commands
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class TurnOnLightCommand(Command):
def __init__(self, light: Light):
self._light = light
def execute(self):
self._light.turn_on()
def undo(self):
self._light.turn_off()
class TurnOffLightCommand(Command):
def __init__(self, light: Light):
self._light = light
def execute(self):
self._light.turn_off()
def undo(self):
self._light.turn_on()
# A command that does nothing (Null Object Pattern)
class NoCommand(Command):
def execute(self):
pass
def undo(self):
pass
# The Invoker: Our remote control
class RemoteControl:
def __init__(self):
self._on_buttons: list[Command] = [NoCommand()] * 3
self._off_buttons: list[Command] = [NoCommand()] * 3
self._undo_command: Command = NoCommand()
def set_command(self, slot: int, on_command: Command, off_command: Command):
self._on_buttons[slot] = on_command
self._off_buttons[slot] = off_command
def press_on_button(self, slot: int):
command = self._on_buttons[slot]
command.execute()
self._undo_command = command
def press_off_button(self, slot: int):
command = self._off_buttons[slot]
command.execute()
self._undo_command = command
def press_undo_button(self):
print("--- UNDO ---")
self._undo_command.undo()
self._undo_command = NoCommand()
# Client Code
light = Light()
light_on = TurnOnLightCommand(light)
light_off = TurnOffLightCommand(light)
remote = RemoteControl()
remote.set_command(0, light_on, light_off)
remote.press_on_button(0) # Light is ON
remote.press_off_button(0) # Light is OFF
remote.press_undo_button() # --- UNDO --- \n Light is ON
Benefits of the Command Pattern
- Decouples the Invoker from the Receiver: The object that initiates an action (
RemoteControl) knows nothing about the object that performs it (Light). It only knows it has aCommandobject with anexecute()method. - Enables Undo/Redo: By storing a history of command objects, you can easily implement multi-level undo and redo functionality.
- Supports Queuing and Asynchronous Operations: Command objects can be stored in a queue and processed later, perhaps by a worker thread or a different process.
- Creates Composable Actions: You can create a
MacroCommandthat holds a list of other commands and executes them in sequence. This allows you to build complex operations from simpler ones. - Improves Testability: You can test the invoker and the receiver in isolation by providing mock command objects.
The Command pattern is a versatile behavioral pattern that adds a layer of abstraction between the “what” and the “how” of an operation. It’s a cornerstone of interactive applications, background job processors, and any system that needs to manage actions as first-class objects.