Introduction
The Command pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of requests. It separates the object that invokes the operation from the object that performs the operation.
This pattern is particularly useful in scenarios requiring:
- Decoupling: Separating the invoker of an operation from its concrete implementation.
- Undo/Redo Functionality: Operations can be stored and reversed.
- Command Queues and Logging: Requests can be queued, executed asynchronously, or logged for auditing.
- Macro Recording: Sequences of commands can be recorded and replayed.
Implementation
The core components of the Command pattern are:
- Command: An interface or abstract class declaring an
execute()
method. - Concrete Command: Implements the
Command
interface, binding aReceiver
object with an action. It typically stores theReceiver
as an instance variable. - Receiver: The object that performs the actual work. It knows how to perform the operations required to carry out the request.
- Invoker: Holds a
Command
object and invokes itsexecute()
method. It does not know the concrete class of the command or the identity of theReceiver
. - Client: Creates a
Concrete Command
object and sets itsReceiver
. It then associates theConcrete Command
with theInvoker
.
Here’s a concise example in Python demonstrating a simple light control system:
import abc
# 1. Command Interface
class Command(abc.ABC):
"""
The Command interface declares a method for executing a command.
"""
@abc.abstractmethod
def execute(self) -> None:
pass
# 2. Receiver
class Light:
"""
The Receiver class contains some business logic.
"""
def turn_on(self) -> None:
print("Light: The light is ON.")
def turn_off(self) -> None:
print("Light: The light is OFF.")
# 3. Concrete Commands
class LightOnCommand(Command):
"""
Concrete Commands implement various kinds of requests.
A Concrete Command stores a reference to the Receiver.
"""
def __init__(self, light: Light) -> None:
self._light = light
def execute(self) -> None:
self._light.turn_on()
class LightOffCommand(Command):
"""
Another Concrete Command for turning the light off.
"""
def __init__(self, light: Light) -> None:
self._light = light
def execute(self) -> None:
self._light.turn_off()
# 4. Invoker
class RemoteControl:
"""
The Invoker is associated with one or several commands.
It sends a request to the command.
"""
def __init__(self) -> None:
self._command: Command = None
def set_command(self, command: Command) -> None:
self._command = command
def press_button(self) -> None:
"""
Executes the set command.
"""
if self._command:
print("RemoteControl: Pressing button...")
self._command.execute()
else:
print("RemoteControl: No command is set.")
# 5. Client
if __name__ == "__main__":
# Create the Receiver
bedroom_light = Light()
# Create Concrete Commands, binding them to the Receiver
light_on_cmd = LightOnCommand(bedroom_light)
light_off_cmd = LightOffCommand(bedroom_light)
# Create the Invoker
remote = RemoteControl()
# Client associates commands with the Invoker and triggers them
remote.set_command(light_on_cmd)
remote.press_button() # Output: Light: The light is ON.
remote.set_command(light_off_cmd)
remote.press_button() # Output: Light: The light is OFF.
# Demonstrates no command set
empty_remote = RemoteControl()
empty_remote.press_button() # Output: RemoteControl: No command is set.
Mermaid Diagram
graph TD; Invoker --> Command; Command --> Receiver;
Figure 1: Simplified Command Pattern Structure
Pros & Cons
Advantages
- Decoupling: Decouples the invoker from the receiver, promoting loose coupling. The invoker does not need to know anything about the operations it executes or the objects that perform them.
- Extensibility: New commands can be added easily without changing existing code. This adheres to the Open/Closed Principle.
- Undo/Redo Functionality: Commands can be easily made reversible by adding an
undo()
method to theCommand
interface, as each command encapsulates all necessary information for its execution and reversal. - Command Queues and Logging: Commands can be stored in a queue, executed asynchronously, or logged for auditing purposes and recovery.
- Composite Commands (Macros): Simple commands can be combined into more complex, composite commands.
Disadvantages
- Increased Complexity: Introduces additional classes and objects for each command, which can increase the overall complexity of the codebase, especially for simple operations.
- Overhead: For very simple applications, the overhead of implementing the Command pattern might outweigh its benefits.
- Potential for Over-engineering: If not applied judiciously, it can lead to unnecessary abstraction where a direct method call would suffice.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Refactoring.Guru. (n.d.). Command Design Pattern. Retrieved from https://refactoring.guru/design-patterns/command
Last updated on