Welcome to the Behavioral Patterns course! In this lesson, we will explore the Command Pattern, a fundamental design pattern that is highly useful for promoting flexible and reusable code. This pattern is particularly effective in scenarios where you need to parameterize objects with operations, queues, or logs.
You might remember from previous lessons that behavioral design patterns help with object communication and responsibility distribution within your software. The Command Pattern encapsulates a request as an object, thereby allowing users to parameterize clients with queues, requests, and operations. This encapsulation enables us to decouple the sender from the receiver, enhancing the flexibility and maintainability of the system.
The Command Pattern involves creating a command interface with an execute
method. We then create concrete command classes that implement this interface, each representing a specific action. Finally, we'll integrate these commands with a request invoker to execute the actions. This structure allows us to easily extend or modify commands without changing the invoker or the receiver.
To understand the Command Pattern, we should first identify its essential components: Command, Receiver, Concrete Commands, and Invoker. Here's a breakdown of each component, along with their implementations. Each of these components plays a critical role in decoupling the sender and receiver, thereby making the system more modular and flexible.
The Command interface declares an execute
method that must be implemented by all concrete commands. This interface will help us define the actions to be executed. This interface enables a consistent method signature for executing various commands, making the system easier to extend.
Python1from abc import ABC, abstractmethod 2 3class Command(ABC): 4 @abstractmethod 5 def execute(self): 6 pass
The receiver is the object that performs the actual action. In our example, the Light
class will serve as the receiver that can turn the light on or off. The receiver contains the actual logic that gets executed when the command is invoked.
Python1class Light: 2 def on(self): 3 print("Light is on.") 4 5 def off(self): 6 print("Light is off.")
Concrete command classes implement the Command interface and are responsible for executing the receiver's methods. They encapsulate the receiver object and invoke the appropriate actions. In our example, we create LightOnCommand
and LightOffCommand
. These concrete commands act as intermediaries, translating user actions into calls to the receiver.
Python1class LightOnCommand(Command): 2 def __init__(self, light): 3 self.light = light 4 5 def execute(self): 6 self.light.on()
Python1class LightOffCommand(Command): 2 def __init__(self, light): 3 self.light = light 4 5 def execute(self): 6 self.light.off()
The invoker is the object that sends a request to execute a command. It holds a command object and can execute it. In our example, the RemoteControl
class is the invoker. The invoker is responsible for triggering the appropriate command based on user actions as it knows nothing about the actual operations that are performed.
Python1class RemoteControl: 2 def __init__(self): 3 self.command = None 4 5 def set_command(self, command): 6 self.command = command 7 8 def press_button(self): 9 if self.command: 10 self.command.execute()
Now that we have broken down the Command Pattern into its core components, let's put it all together in a cohesive example. The RemoteControl
(invoker) holds and triggers the command objects without knowing their implementation details. The LightOnCommand
and LightOffCommand
(concrete commands) encapsulate actions on the Light
(receiver), translating invoker requests into specific receiver operations. This setup decouples the sender (invoker) from the receiver, promoting flexibility and extensibility in the system. This example demonstrates how encapsulating requests as objects can significantly simplify the design of a system with multiple actions and receivers.
Python1from abc import ABC, abstractmethod 2 3class Command(ABC): 4 @abstractmethod 5 def execute(self): 6 pass 7 8class Light: 9 def on(self): 10 print("Light is on.") 11 12 def off(self): 13 print("Light is off.") 14 15class LightOnCommand(Command): 16 def __init__(self, light): 17 self.light = light 18 19 def execute(self): 20 self.light.on() 21 22class LightOffCommand(Command): 23 def __init__(self, light): 24 self.light = light 25 26 def execute(self): 27 self.light.off() 28 29class RemoteControl: 30 def __init__(self): 31 self.command = None 32 33 def set_command(self, command): 34 self.command = command 35 36 def press_button(self): 37 if self.command: 38 self.command.execute() 39 40if __name__ == "__main__": 41 light = Light() 42 light_on = LightOnCommand(light) 43 light_off = LightOffCommand(light) 44 45 remote = RemoteControl() 46 remote.set_command(light_on) 47 remote.press_button() # Output: Light is on. 48 remote.set_command(light_off) 49 remote.press_button() # Output: Light is off.
To make the example more dynamic and extendable while maintaining the proper usage of the Command Pattern with an invoker, we can introduce a dictionary within the invoker to manage our command objects. This adjustment allows us to easily look up and execute commands based on keys while still preserving the invoker's role. Here’s an updated version of the main section:
Python1class RemoteControl: 2 def __init__(self): 3 self.commands = {} 4 5 def set_command(self, name, command): 6 self.commands[name] = command 7 8 def press_button(self, name): 9 if name in self.commands: 10 self.commands[name].execute() 11 else: 12 print(f"No command named '{name}' found.") 13 14if __name__ == "__main__": 15 light = Light() 16 light_on = LightOnCommand(light) 17 light_off = LightOffCommand(light) 18 19 remote = RemoteControl() 20 remote.set_command("lightOn", light_on) 21 remote.set_command("lightOff", light_off) 22 23 # Simulate user input 24 remote.press_button("lightOn") # Output: Light is on. 25 remote.press_button("lightOff") # Output: Light is off.
In this enhancement, commands are stored in a dictionary within the RemoteControl
(invoker), simplifying command referencing and execution via keys. This structure maintains the integrity of the Command Pattern, ensuring that the invoker triggers the commands. This dynamic structure helps handle various inputs and extends the system with ease.
Understanding and applying the Command Pattern is vital for writing maintainable and scalable code. This pattern allows you to decouple the sender of a request from its receiver, which can lead to more modular and easier-to-maintain systems. Consider a smart home system where various devices can be controlled via commands. By using the Command Pattern, you can seamlessly add new commands for different devices without altering existing code. This flexibility reduces the risk of bugs and simplifies code management, making your software more robust and easier to extend. Implementing this pattern can significantly improve the design and flexibility of your software architecture.