Command Pattern
The Command Pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing for parameterizing clients with queues, requests, and operations. It promotes loose coupling between objects by decoupling the object that invokes the operation from the one that knows how to perform it. This pattern also supports undoable operations, logging, and queuing of requests.
What is Command Pattern
The Command Pattern addresses the need to treat operations as first-class objects. Instead of directly calling methods on an object, you create a Command object that encapsulates the action to be performed. This command object knows about the receiver (the object on which the action will be performed) and the action itself.
The core idea is to separate the invocation of a request from its execution. This separation allows for:
- Flexibility: You can easily add new commands without modifying existing code.
- Extensibility: You can combine simple commands into complex composite commands (macros).
- Undo/Redo: By storing a history of executed commands, you can implement undo and redo functionality.
- Queuing and Scheduling: You can queue commands for later execution or schedule them to run at specific times.
- Logging: You can log commands for auditing or debugging purposes.
Considerations:
- Increased Complexity: The pattern introduces additional classes, which can increase the complexity of the codebase, especially in simple scenarios.
- Memory Overhead: Storing command objects, especially for undo/redo functionality, can consume significant memory. This is crucial in resource-constrained environments.
- Performance: Creating and executing command objects might introduce a slight performance overhead compared to direct method calls. However, this overhead is usually negligible in most applications, and the benefits of flexibility and maintainability often outweigh the performance cost. Careful design is needed to minimize object creation and destruction.
- Thread Safety: If commands are executed in a multi-threaded environment, you need to ensure thread safety, especially when accessing shared resources within the commandās
execute()method. - Serialization: If you need to serialize commands (e.g., for persisting them to disk or sending them over a network), you need to implement serialization logic for each command class.
Syntax and Usage
The Command Pattern typically involves the following components:
-
Command (Interface/Abstract Class): Declares an interface for executing an operation.
class Command { public: virtual void execute() = 0; virtual ~Command() {} // Virtual destructor for proper cleanup }; -
ConcreteCommand: Defines a binding between a Receiver object and an action. It implements the
execute()method by invoking the corresponding operation(s) on the Receiver.class ConcreteCommand : public Command { private: class Receiver* receiver; // Data needed to execute the command int argument; public: ConcreteCommand(Receiver* receiver, int arg) : receiver(receiver), argument(arg) {} void execute() override { receiver->action(argument); } }; -
Receiver: Knows how to perform the operations associated with carrying out a request. Any class can serve as a Receiver.
class Receiver { public: void action(int arg) { // Perform the action std::cout << "Receiver: Performing action with argument " << arg << std::endl; } }; -
Invoker: Asks the command to carry out the request. It holds a
Commandobject and calls itsexecute()method when appropriate.class Invoker { private: Command* command; public: void setCommand(Command* command) { this->command = command; } void executeCommand() { command->execute(); } };
Basic Example
Letās consider a simple example of a light switch. The light is the receiver, and the commands are āturn onā and āturn offā.
#include <iostream>
#include <vector>
// Receiver
class Light {
public:
void turnOn() {
std::cout << "Light is ON" << std::endl;
}
void turnOff() {
std::cout << "Light is OFF" << std::endl;
}
};
// Command Interface
class Command {
public:
virtual void execute() = 0;
virtual ~Command() {}
};
// Concrete Commands
class TurnOnCommand : public Command {
private:
Light* light;
public:
TurnOnCommand(Light* light) : light(light) {}
void execute() override {
light->turnOn();
}
};
class TurnOffCommand : public Command {
private:
Light* light;
public:
TurnOffCommand(Light* light) : light(light) {}
void execute() override {
light->turnOff();
}
};
// Invoker
class Switch {
private:
std::vector<Command*> history;
Light* light; // Store light pointer
public:
Switch(Light* light) : light(light) {}
void executeCommand(Command* cmd) {
cmd->execute();
history.push_back(cmd);
}
void undoLastCommand() {
if (!history.empty()) {
//Create opposite command and execute it
Command* lastCommand = history.back();
history.pop_back();
if (dynamic_cast<TurnOnCommand*>(lastCommand)) {
TurnOffCommand* undoCmd = new TurnOffCommand(light);
undoCmd->execute();
delete undoCmd;
} else if (dynamic_cast<TurnOffCommand*>(lastCommand)) {
TurnOnCommand* undoCmd = new TurnOnCommand(light);
undoCmd->execute();
delete undoCmd;
}
delete lastCommand;
}
}
};
int main() {
Light* light = new Light();
Switch mySwitch(light);
Command* turnOn = new TurnOnCommand(light);
Command* turnOff = new TurnOffCommand(light);
mySwitch.executeCommand(turnOn); // Light is ON
mySwitch.executeCommand(turnOff); // Light is OFF
mySwitch.executeCommand(turnOn); // Light is ON
mySwitch.undoLastCommand(); // Light is OFF
delete turnOn;
delete turnOff;
delete light;
return 0;
}This code demonstrates the basic implementation of the Command Pattern. The Light class is the receiver. TurnOnCommand and TurnOffCommand are concrete commands that encapsulate the āturn onā and āturn offā actions, respectively. The Switch class is the invoker, which executes the commands. The undoLastCommand function demonstrates a simple undo functionality.
Advanced Example
Consider a text editor application. Operations like āopen fileā, āsave fileā, ācutā, ācopyā, āpasteā can be implemented as commands. The Document class acts as the receiver. This example also shows how to handle command history for undo/redo.
#include <iostream>
#include <string>
#include <vector>
// Receiver
class Document {
private:
std::string content;
public:
void open() {
std::cout << "Opening document..." << std::endl;
content = "Initial document content.";
}
void save() {
std::cout << "Saving document..." << std::endl;
}
void cut() {
std::cout << "Cutting text..." << std::endl;
content = ""; // Simulate cutting the entire content
}
void paste() {
std::cout << "Pasting text..." << std::endl;
content = "Pasted content";
}
std::string getContent() const {
return content;
}
void setContent(const std::string& newContent) {
content = newContent;
}
};
// Command Interface
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() {}
};
// Concrete Commands
class OpenCommand : public Command {
private:
Document* document;
public:
OpenCommand(Document* document) : document(document) {}
void execute() override {
document->open();
}
void undo() override {
//Opening a file cannot reasonably be undone
std::cout << "Cannot undo open command" << std::endl;
}
};
class SaveCommand : public Command {
private:
Document* document;
public:
SaveCommand(Document* document) : document(document) {}
void execute() override {
document->save();
}
void undo() override {
//Saving a file cannot reasonably be undone
std::cout << "Cannot undo save command" << std::endl;
}
};
class CutCommand : public Command {
private:
Document* document;
std::string backup;
public:
CutCommand(Document* document) : document(document) {}
void execute() override {
backup = document->getContent();
document->cut();
}
void undo() override {
document->setContent(backup);
}
};
class PasteCommand : public Command {
private:
Document* document;
std::string backup;
public:
PasteCommand(Document* document) : document(document) {}
void execute() override {
backup = document->getContent();
document->paste();
}
void undo() override {
document->setContent(backup);
}
};
// Invoker
class Editor {
private:
std::vector<Command*> history;
Document* document;
public:
Editor(Document* document) : document(document) {}
void executeCommand(Command* command) {
command->execute();
history.push_back(command);
}
void undo() {
if (!history.empty()) {
Command* command = history.back();
history.pop_back();
command->undo();
delete command;
}
}
};
int main() {
Document* document = new Document();
Editor editor(document);
Command* openCommand = new OpenCommand(document);
Command* cutCommand = new CutCommand(document);
Command* pasteCommand = new PasteCommand(document);
editor.executeCommand(openCommand);
editor.executeCommand(cutCommand);
std::cout << "Content after cut: " << document->getContent() << std::endl;
editor.executeCommand(pasteCommand);
std::cout << "Content after paste: " << document->getContent() << std::endl;
editor.undo(); // Undo paste
std::cout << "Content after undo paste: " << document->getContent() << std::endl;
editor.undo(); // Undo cut
std::cout << "Content after undo cut: " << document->getContent() << std::endl;
delete openCommand;
delete cutCommand;
delete pasteCommand;
delete document;
return 0;
}This advanced example shows how the command pattern can be used to implement more complex features like undo/redo. Each command now has an undo() method. The Editor class maintains a history of executed commands, allowing it to undo them in reverse order. The backup variable in CutCommand and PasteCommand is used to store the previous state of the document, enabling the undo() operation to restore the document to its previous state.
Common Use Cases
- Undo/Redo Functionality: As demonstrated in the advanced example, itās ideal for implementing undo/redo features in applications.
- Transaction Processing: Encapsulating database operations as commands allows for easy rollback in case of errors.
- GUI Button Actions: Mapping GUI button clicks to command objects provides a flexible way to handle user interactions.
- Macro Recording: Recording a sequence of commands to replay them later.
- Asynchronous Task Execution: Queuing commands for execution in a separate thread or process.
Best Practices
- Keep Commands Small and Focused: Each command should represent a single, well-defined action.
- Consider Command Groups (Macros): Combine multiple commands into a single composite command for complex operations.
- Implement Undo Carefully: Ensure that the
undo()method correctly reverses the effects of theexecute()method, especially when dealing with complex state changes. - Manage Command History: Implement a robust mechanism for storing and managing the history of executed commands, considering memory usage and performance.
- Use Smart Pointers: Utilize smart pointers like
std::unique_ptrorstd::shared_ptrto manage the lifetime of command objects and prevent memory leaks.
Common Pitfalls
- Overuse: Applying the Command Pattern to simple scenarios can add unnecessary complexity. Consider whether the benefits of flexibility and extensibility outweigh the added overhead.
- Memory Leaks: Failing to properly manage the lifetime of command objects can lead to memory leaks, especially when storing a history of commands for undo/redo functionality.
- Complex Undo Logic: Implementing the
undo()method can be challenging for commands that involve complex state changes or external resources. - Thread Safety Issues: In multi-threaded environments, ensure that commands are thread-safe, especially when accessing shared resources within the
execute()andundo()methods.
Key Takeaways
- The Command Pattern decouples the invoker of a request from the object that performs it.
- It allows treating operations as first-class objects, enabling flexibility and extensibility.
- It supports undo/redo functionality, queuing, logging, and other advanced features.
- Careful consideration is needed to avoid overuse and potential pitfalls such as memory leaks and thread safety issues.