Command Pattern¶
Overview¶
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 allows you to pass requests as method arguments, delay or queue a request's execution, and support undoable operations.
Intent¶
- Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations
- Decouple the object that invokes the operation from the object that knows how to perform it
- Allow requests to be queued, logged, or undone
Problem¶
Imagine you're working on a text editor application. You want to create buttons for various operations: copy, paste, undo, redo, save, etc. You initially implement this by having each button directly call methods on the editor object.
Problems arise when you need to: - Implement undo/redo functionality - Queue operations for later execution - Log operations for audit purposes - Support keyboard shortcuts and menu items for the same operations - Create macro commands that combine multiple operations
The direct coupling between UI elements and business logic makes the system rigid and difficult to extend.
Solution¶
The Command pattern suggests that GUI objects shouldn't send requests directly to business logic objects. Instead, you should extract all request details, such as the object being called, the name of the method, and the list of arguments, into a separate command class with a single method that triggers this request.
Command objects serve as links between various GUI and business logic objects. From now on, the GUI object doesn't need to know what business logic object will receive the request and how it'll be processed. The GUI object just triggers the command, which handles all the details.
Structure¶
Components¶
-
Command Interface: Declares an interface for executing an operation and optionally undoing it.
-
ConcreteCommand: Implements the Command interface and defines a binding between a Receiver object and an action. Implements execute() by invoking corresponding operations on Receiver.
-
Receiver: Knows how to perform the operations associated with carrying out a request. Any class can serve as a Receiver.
-
Invoker: Asks the command to carry out the request. Doesn't know anything about the concrete command.
-
Client: Creates a ConcreteCommand object and sets its receiver.
Implementation Details¶
Basic Command Interface¶
Simple Command¶
A simple command that doesn't require a receiver:
Complex Command with Receiver¶
A complex command that delegates work to a receiver:
Invoker¶
The invoker stores and executes commands:
Command History (Undo/Redo)¶
Real-World Example: Text Editor¶
Implementation¶
Applicability¶
Use the Command pattern when you want to:
-
Parameterize objects with operations: Command lets you parameterize objects with operations. You can replace a callback function with a Command object.
-
Queue operations: You can queue commands for execution, schedule their execution, or execute them remotely.
-
Implement undo/redo: Commands can store state for reversing their effect. The Command interface must have an added
undooperation that reverses the effects of a previous call toexecute. -
Log operations: You can log commands as they're executed and replay them in case of a system crash. By augmenting the Command interface with load and store operations, you can keep a persistent log of changes.
-
Structure a system around high-level operations: You can build a system around high-level operations built on primitive operations. Such structure is common in information systems that support transactions.
Advantages¶
-
Single Responsibility Principle: You can decouple classes that invoke operations from classes that perform these operations.
-
Open/Closed Principle: You can introduce new commands into the app without breaking existing client code.
-
Undo/Redo: You can implement undo/redo functionality.
-
Deferred execution: You can defer command execution or queue commands.
-
Assemble complex commands: You can assemble a set of simple commands into a complex one (Composite pattern).
-
Logging and auditing: You can log commands for auditing or crash recovery purposes.
Disadvantages¶
-
Code complexity: The code may become more complicated since you're introducing a whole new layer between senders and receivers.
-
Increased number of classes: Each individual command is a class, which increases the number of classes in your application.
-
Memory overhead: If you're storing command history for undo/redo, it can consume significant memory for large operations.
Relations with Other Patterns¶
- Chain of Responsibility: Handlers in Chain of Responsibility can be implemented as Commands. In this case, you can execute many different operations over the same context object, represented by a request.
- Memento: You can use Memento together with Command for implementing undo. In this case, commands are responsible for performing various operations over a target object, while mementos save the state of that object just before a command gets executed.
- Strategy: Both patterns can be used to parameterize an object with some action. However, they have different intents:
- Command is about turning a request into an object with all information needed to perform an action
- Strategy is about different ways of doing the same thing
- Prototype: You can use Prototype when you need to save copies of Commands into history for undo/redo.
- Composite: Macro commands can be implemented using the Composite pattern.
- Visitor: You can treat Visitor as a powerful version of the Command pattern. Its objects can execute operations over various objects of different classes.
Best Practices¶
-
Keep commands simple: Each command should encapsulate a single operation. For complex operations, use macro commands.
-
Store minimal state: Commands should store only the minimum state needed to execute and undo the operation.
-
Consider memory usage: If implementing undo/redo, consider the memory impact of storing command history. You may need to limit history size.
-
Use smart pointers: Use smart pointers (std::shared_ptr, std::unique_ptr) for memory management.
-
Make commands immutable: Once a command is created, its parameters shouldn't change.
-
Implement proper undo: Ensure that undo properly reverses the effect of execute. Test undo/redo cycles thoroughly.
-
Consider command serialization: For logging or network transmission, commands should be serializable.
-
Use command queues: For asynchronous operations, use command queues with proper synchronization.
Example Usage Scenarios¶
1. GUI Applications¶
2. Transaction System¶
3. Task Scheduler¶
4. Macro Commands¶
Conclusion¶
The Command pattern is a powerful tool for decoupling the sender of a request from its receiver. It's particularly useful when you need to: - Implement undo/redo functionality - Queue or schedule operations - Log operations for auditing - Support multiple ways to trigger the same operation
While it adds complexity by introducing additional classes, the benefits in terms of flexibility, maintainability, and extensibility often outweigh the costs, especially in applications with complex user interactions or transaction requirements.