Table of Contents
- What is the Command Pattern?
- Core Components of the Command Pattern
- A Practical Example: Building a Text Editor
- Key Benefits of the Command Pattern
- Advanced Use Cases
- Command Pattern vs. Other Patterns
- Best Practices for Implementing Command Patterns in Python
- Conclusion
- References
What is the Command Pattern?
The Command Pattern is a behavioral design pattern that turns a request into a stand-alone object. This object encapsulates all information needed to perform the request, including:
- The method to call.
- The arguments to pass.
- The receiver (the object that will perform the action).
In formal terms, the Gang of Four (GoF) defines the Command Pattern as:
“Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.”
At its core, the pattern solves a critical problem: decoupling the “sender” of a request from the “receiver” that executes it. The sender (called the “invoker”) doesn’t need to know how the action is performed—only that the command object can execute it.
Core Components of the Command Pattern
To implement the Command Pattern, you’ll typically work with five key components. Let’s break them down:
1. Command Interface
An abstract base class (ABC) or protocol that declares a method for executing a command (e.g., execute()). It may also include a method for undoing the command (e.g., undo()).
2. Concrete Command
A class that implements the Command Interface. It binds a specific action to a receiver and defines execute() to invoke the receiver’s method. It may also store state needed to reverse the action (for undo()).
3. Receiver
The object that performs the actual work. It contains the business logic for the action (e.g., a TextEditor class with copy(), paste() methods).
4. Invoker
The object that “asks” the command to carry out the request. It holds a reference to a command and calls execute() on it. The invoker may also manage a history of commands to support undo/redo.
5. Client
The object that creates Concrete Command instances and assigns their receivers. It connects the invoker to the appropriate commands.
A Practical Example: Building a Text Editor
Let’s bring these components to life with a real-world example: a simple text editor that supports copy, paste, and undo operations. We’ll implement each component step by step.
Step 1: Define the Receiver
The receiver is the TextEditor class, which contains the actual logic for editing text. It will have methods like copy(), paste(), and get_selected_text() to interact with the text buffer.
class TextEditor:
def __init__(self):
self.buffer = "" # Main text buffer
self.clipboard = "" # Clipboard for copy/paste
self.selection = (0, 0) # (start, end) indices of selected text
def set_selection(self, start: int, end: int) -> None:
"""Set the selected text range."""
self.selection = (start, end)
def get_selected_text(self) -> str:
"""Return the currently selected text."""
start, end = self.selection
return self.buffer[start:end]
def copy(self) -> None:
"""Copy selected text to clipboard."""
self.clipboard = self.get_selected_text()
print(f"Copied to clipboard: '{self.clipboard}'")
def paste(self) -> None:
"""Paste clipboard content at the end of the buffer."""
self.buffer += self.clipboard
print(f"Pasted. Buffer: '{self.buffer}'")
def delete_selected(self) -> str:
"""Delete selected text and return it (for undo support)."""
start, end = self.selection
deleted_text = self.buffer[start:end]
self.buffer = self.buffer[:start] + self.buffer[end:]
return deleted_text
def insert_text(self, text: str, position: int) -> None:
"""Insert text at a specific position (for undo support)."""
self.buffer = self.buffer[:position] + text + self.buffer[position:]
Step 2: Create the Command Interface
Next, we’ll define a Command interface using Python’s abc.ABC to enforce the execute() and undo() methods.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None:
"""Execute the command."""
pass
@abstractmethod
def undo(self) -> None:
"""Undo the command."""
pass
Step 3: Implement Concrete Commands
Now, we’ll create Concrete Command classes for CopyCommand and PasteCommand. Each will wrap a specific action of the TextEditor receiver.
CopyCommand
class CopyCommand(Command):
def __init__(self, editor: TextEditor):
self.editor = editor # Receiver
def execute(self) -> None:
# Store state needed for undo (copy doesn't modify buffer, so undo is a no-op)
self.prev_clipboard = self.editor.clipboard
self.editor.copy()
def undo(self) -> None:
# Restore clipboard to previous state
self.editor.clipboard = self.prev_clipboard
print(f"Undid copy. Clipboard restored to: '{self.prev_clipboard}'")
PasteCommand
Paste modifies the buffer, so we need to track the buffer’s state before pasting to support undo:
class PasteCommand(Command):
def __init__(self, editor: TextEditor):
self.editor = editor # Receiver
def execute(self) -> None:
# Store state needed for undo: buffer length before paste
self.prev_buffer_length = len(self.editor.buffer)
self.editor.paste()
def undo(self) -> None:
# Remove the pasted text by truncating the buffer
self.editor.buffer = self.editor.buffer[:self.prev_buffer_length]
print(f"Undid paste. Buffer restored to: '{self.editor.buffer}'")
Step 4: Build the Invoker
The invoker (EditorInvoker) will manage commands, execute them, and track history for undo.
class EditorInvoker:
def __init__(self):
self.command_history: list[Command] = []
def execute_command(self, command: Command) -> None:
"""Execute a command and add it to history."""
command.execute()
self.command_history.append(command)
def undo_last(self) -> None:
"""Undo the last executed command."""
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()
else:
print("No commands to undo.")
Step 5: Wire It All Together with the Client
Finally, the client will set up the receiver, commands, and invoker, then simulate user interactions.
if __name__ == "__main__":
# Client setup
editor = TextEditor()
invoker = EditorInvoker()
# Simulate user actions
editor.buffer = "Hello, "
editor.set_selection(0, 7) # Select "Hello, "
# Execute CopyCommand
copy_cmd = CopyCommand(editor)
invoker.execute_command(copy_cmd) # Output: Copied to clipboard: 'Hello, '
# Execute PasteCommand
paste_cmd = PasteCommand(editor)
invoker.execute_command(paste_cmd) # Output: Pasted. Buffer: 'Hello, Hello, '
# Undo the last command (Paste)
invoker.undo_last() # Output: Undid paste. Buffer restored to: 'Hello, '
# Undo the previous command (Copy)
invoker.undo_last() # Output: Undid copy. Clipboard restored to: ''
Output
Copied to clipboard: 'Hello, '
Pasted. Buffer: 'Hello, Hello, '
Undid paste. Buffer restored to: 'Hello, '
Undid copy. Clipboard restored to: ''
Key Benefits of the Command Pattern
The example above highlights several advantages of the Command Pattern:
-
Decoupling: The invoker (
EditorInvoker) doesn’t know howcopyorpastework—it only callsexecute(). This makes it easy to add new commands (e.g.,CutCommand) without changing the invoker. -
Undo/Redo: By tracking command history and storing state in
Concrete Commandobjects, we can easily reverse actions. -
Queuing and Scheduling: Commands can be stored in a queue and executed later (e.g., batch processing).
-
Extensibility: Adding new commands (e.g.,
DeleteCommand,FormatCommand) requires only a newConcrete Commandclass, adhering to the Open/Closed Principle. -
Logging and Auditing: Commands can log their execution for debugging or compliance (e.g., “User X executed PasteCommand at 14:30”).
Advanced Use Cases
Macros: Chaining Commands
You can create a MacroCommand to execute a sequence of commands as a single unit. For example, a “Save and Close” macro that runs SaveCommand followed by CloseCommand.
class MacroCommand(Command):
def __init__(self, commands: list[Command]):
self.commands = commands
def execute(self) -> None:
for cmd in self.commands:
cmd.execute()
def undo(self) -> None:
# Undo in reverse order
for cmd in reversed(self.commands):
cmd.undo()
# Usage:
macro = MacroCommand([copy_cmd, paste_cmd, paste_cmd])
invoker.execute_command(macro) # Executes copy, paste, paste
invoker.undo_last() # Undoes paste, paste, copy
Asynchronous Commands
For long-running tasks (e.g., file downloads), wrap commands in asyncio coroutines:
import asyncio
class AsyncCommand(Command):
async def execute(self) -> None:
# Simulate async work (e.g., API call)
await asyncio.sleep(1)
print("Async command executed.")
async def undo(self) -> None:
await asyncio.sleep(1)
print("Async command undone.")
Logging for Audit Trails
Modify execute() to log actions to a file or database:
import datetime
class LoggedCommand(Command):
def execute(self) -> None:
self.timestamp = datetime.datetime.now()
super().execute() # Call concrete command's execute
with open("audit.log", "a") as f:
f.write(f"[{self.timestamp}] Executed {self.__class__.__name__}\n")
Command Pattern vs. Other Patterns
It’s important to distinguish the Command Pattern from similar patterns:
- Strategy Pattern: Focuses on algorithm selection (e.g., different sorting algorithms). Command focuses on action encapsulation (e.g., copy, paste).
- Observer Pattern: Notifies multiple objects of state changes. Command focuses on decoupling request senders and receivers.
- Memento Pattern: Captures an object’s state for later restoration (used with Command for undo/redo).
Best Practices for Implementing Command Patterns in Python
- Keep Commands Lightweight: Commands should focus on what to do, not how to do it. Delegate logic to the receiver.
- Implement Undo Carefully: Store only the minimal state needed to reverse an action (e.g.,
PasteCommandstores the buffer length before pasting). - Use Interfaces: Enforce
execute()/undo()withabc.ABCto avoid runtime errors. - Test Commands in Isolation: Since commands are decoupled, you can unit-test them without their invoker or receiver.
- Avoid Side Effects: Ensure
execute()andundo()are idempotent (repeating them has the same effect as once).
Conclusion
The Command Pattern is a powerful tool for building flexible, maintainable applications. By encapsulating actions as objects, it enables undo/redo, batch operations, and extensibility—all while keeping your codebase modular. Whether you’re building a text editor, a workflow engine, or a home automation system, the Command Pattern will help you design systems that adapt to change with ease.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Software Foundation. (n.d.). abc — Abstract Base Classes. https://docs.python.org/3/library/abc.html
- Refactoring Guru. (n.d.). Command Pattern. https://refactoring.guru/design-patterns/command
- Real Python. (2020). Python Design Patterns: Command. https://realpython.com/python-command-pattern/