The Command design pattern is a behavioral design pattern in which an object is used to encapsulate all the information needed to perform an action or trigger an event at a later time. This pattern allows for the separation of concerns, making it easier to handle, queue, and log operations.
-
Command Interface: The command interface declares a method for executing the command. Typically, this is named
Execute()or similar. This interface might also include anUndo()method to allow reversing the operation if needed. -
Concrete Command: A concrete command class implements the command interface and defines the binding between an action and a receiver. It holds a reference to the receiver object, which performs the action when the command’s
Execute()method is called. This class can also implement anUndo()method to reverse the operation. -
Receiver: The receiver is the object that performs the actual work when the command is executed. It contains the business logic necessary to carry out the request. The receiver doesn’t know anything about the command interface, only how to perform the requested action.
-
Invoker: The invoker is responsible for initiating requests or commands. It holds a reference to the command object and triggers its
Execute()method. The invoker doesn’t know the details of how the request will be handled, only that it will be executed when needed. -
Client: The client is responsible for creating command objects and configuring them with the appropriate receiver. The client decides which command to execute and passes it to the invoker.
-
Decoupling of Sender and Receiver: The Command pattern decouples the object that invokes the operation (invoker) from the one that knows how to perform it (receiver). This allows for greater flexibility, as commands can be passed around, queued, or stored without needing to know anything about the implementation details.
-
Support for Undo/Redo: By encapsulating operations in objects, the Command pattern naturally supports undo/redo functionality. Each command can store state information that allows it to reverse the operation if needed, which is useful in scenarios like text editors or transaction systems.
-
Command History and Queuing: Commands can be stored in a history list or queue, allowing for deferred execution, batch processing, or replaying a sequence of commands. This is particularly useful in scenarios where you need to maintain a log of operations or execute commands later.
-
Extensibility: The Command pattern makes it easy to add new commands without altering existing classes. New commands can be created by simply implementing the command interface and defining the
Execute()method, without needing to modify the invoker or receiver. -
Macro Commands: The Command pattern supports macro commands, which are commands that execute a sequence of other commands. This allows grouping multiple operations into a single command, which can then be executed or undone as a single unit.
-
Command Interface:
ICommandinterface defines the Execute and Undo methods, which concrete command classes will implement. -
Receiver:
FileSystemReceiverclass simulates the file system in an operating system. It provides methods likeCreateFile,WriteFile, andDeleteFileto perform actual operations. -
Concrete Command Classes:
CreateFileCommand: Encapsulates the action of creating a file.WriteFileCommand: Encapsulates the action of writing content to a file.DeleteFileCommand: Encapsulates the action of deleting a file.
Each of these classes contains a reference to the FileSystemReceiver and uses it to perform the specific operation when Execute is called. They also implement Undo to reverse the operation.
- Invoker:
FileInvokerruns commands and keeps a history of the ones it has run. It never learns what a command does or which receiver carries it out, which is what lets it queue requests, undo the last one, or roll back everything.
class Program
{
static void Main(string[] args)
{
// Receiver
var fileSystem = new FileSystemReceiver();
// Commands
var createCommand = new CreateFileCommand(fileSystem, "example.txt");
var writeCommand = new WriteFileCommand(fileSystem, "example.txt", "Hello, World!");
var deleteCommand = new DeleteFileCommand(fileSystem, "example.txt");
// Invoker: it records what it runs, so it can take the calls back
var fileInvoker = new FileInvoker();
fileInvoker.Execute(createCommand); // Creating file: example.txt
fileInvoker.Execute(writeCommand); // Writing to file: example.txt
fileInvoker.Execute(deleteCommand); // Deleting file: example.txt
// Three commands are on the history, most recent last
Console.WriteLine(fileInvoker.History.Count); // 3
fileInvoker.Undo(); // Undoing delete: Recreating file example.txt
fileInvoker.UndoAll(); // Undoes the write, then the create
}
}