Command Design Pattern Tutorial
Introduction
The Command Design Pattern is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This transformation allows you to:
- Parameterize objects with operations
- Queue or log requests
- Support undoable operations
- Compose simple commands into complex ones
At its core, the Command pattern encapsulates a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
Real-World Analogy: Restaurant Order System
Imagine you’re at a restaurant:
- Customer (Client): You decide what food you want to order
- Waiter (Invoker): Takes your order and passes it to the kitchen
- Order Slip (Command): Contains all details about what to prepare
- Chef (Receiver): Receives the order slip and prepares the food
The key insight here is that the waiter doesn’t need to know how to cook the food. They just take the order (command) and pass it to the chef. The order slip contains all the necessary information for the chef to prepare the meal.
Benefits of this approach: - The waiter doesn’t need to know how to cook - The chef doesn’t need to interact with customers - Orders can be queued during busy times - Orders can be logged for inventory management - Special instructions can be added to orders
Command Pattern Structure
The Command pattern typically consists of:
- Command: An interface with an execute method
- Concrete Command: Implements the Command interface and defines the binding between a Receiver object and an action
- Client: Creates the ConcreteCommand and sets its receiver
- Invoker: Asks the command to carry out the request
- Receiver: Knows how to perform the operations
Python Implementation
Let’s implement a simple example of the Command pattern in Python:
from abc import ABC, abstractmethod
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Receiver
class Light:
def __init__(self, location):
self.location = location
self.is_on = False
def turn_on(self):
self.is_on = True
print(f"{self.location} light is now ON")
def turn_off(self):
self.is_on = False
print(f"{self.location} light is now OFF")
# Concrete Commands
class LightOnCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_on()
def undo(self):
self.light.turn_off()
class LightOffCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_off()
def undo(self):
self.light.turn_on()
# Invoker
class RemoteControl:
def __init__(self):
self.command = None
self.history = []
def set_command(self, command):
self.command = command
def press_button(self):
if self.command:
self.command.execute()
self.history.append(self.command)
def press_undo_button(self):
if self.history:
last_command = self.history.pop()
last_command.undo()
# Client code
if __name__ == "__main__":
# Create receivers
living_room_light = Light("Living Room")
kitchen_light = Light("Kitchen")
# Create commands
living_room_light_on = LightOnCommand(living_room_light)
living_room_light_off = LightOffCommand(living_room_light)
kitchen_light_on = LightOnCommand(kitchen_light)
kitchen_light_off = LightOffCommand(kitchen_light)
# Create invoker
remote = RemoteControl()
# Execute commands
remote.set_command(living_room_light_on)
remote.press_button() # Living Room light is now ON
remote.set_command(kitchen_light_on)
remote.press_button() # Kitchen light is now ON
remote.set_command(living_room_light_off)
remote.press_button() # Living Room light is now OFF
# Undo last command
remote.press_undo_button() # Living Room light is now ON
More Complex Example: Smart Home Automation
Let’s expand our example to a more complex smart home automation system:
from abc import ABC, abstractmethod
import time
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Receivers
class Light:
def __init__(self, location):
self.location = location
self.is_on = False
self.brightness = 0
def turn_on(self):
self.is_on = True
self.brightness = 100
print(f"{self.location} light is now ON at {self.brightness}% brightness")
def turn_off(self):
self.is_on = False
self.brightness = 0
print(f"{self.location} light is now OFF")
def dim(self, level):
if 0 <= level <= 100:
self.brightness = level
if level > 0:
self.is_on = True
else:
self.is_on = False
print(f"{self.location} light is now at {self.brightness}% brightness")
class Thermostat:
def __init__(self, location):
self.location = location
self.temperature = 72 # Default temperature in Fahrenheit
def set_temperature(self, temperature):
self.temperature = temperature
print(f"{self.location} thermostat is set to {self.temperature}°F")
class MusicPlayer:
def __init__(self):
self.is_playing = False
self.volume = 50
self.track = None
def play(self, track):
self.is_playing = True
self.track = track
print(f"Playing '{self.track}' at volume {self.volume}%")
def stop(self):
if self.is_playing:
self.is_playing = False
print(f"Stopped playing '{self.track}'")
def set_volume(self, volume):
if 0 <= volume <= 100:
self.volume = volume
print(f"Volume set to {self.volume}%")
# Concrete Commands
class LightOnCommand(Command):
def __init__(self, light):
self.light = light
self.prev_brightness = 0
def execute(self):
self.prev_brightness = self.light.brightness
self.light.turn_on()
def undo(self):
if self.prev_brightness == 0:
self.light.turn_off()
else:
self.light.dim(self.prev_brightness)
class LightOffCommand(Command):
def __init__(self, light):
self.light = light
self.prev_brightness = 0
def execute(self):
self.prev_brightness = self.light.brightness
self.light.turn_off()
def undo(self):
if self.prev_brightness > 0:
self.light.dim(self.prev_brightness)
class DimLightCommand(Command):
def __init__(self, light, level):
self.light = light
self.level = level
self.prev_level = 0
def execute(self):
self.prev_level = self.light.brightness
self.light.dim(self.level)
def undo(self):
self.light.dim(self.prev_level)
class SetThermostatCommand(Command):
def __init__(self, thermostat, temperature):
self.thermostat = thermostat
self.temperature = temperature
self.prev_temperature = 0
def execute(self):
self.prev_temperature = self.thermostat.temperature
self.thermostat.set_temperature(self.temperature)
def undo(self):
self.thermostat.set_temperature(self.prev_temperature)
class PlayMusicCommand(Command):
def __init__(self, music_player, track):
self.music_player = music_player
self.track = track
self.was_playing = False
self.prev_track = None
def execute(self):
self.was_playing = self.music_player.is_playing
self.prev_track = self.music_player.track
self.music_player.play(self.track)
def undo(self):
if self.was_playing and self.prev_track:
self.music_player.play(self.prev_track)
else:
self.music_player.stop()
class StopMusicCommand(Command):
def __init__(self, music_player):
self.music_player = music_player
self.was_playing = False
self.prev_track = None
def execute(self):
self.was_playing = self.music_player.is_playing
self.prev_track = self.music_player.track
self.music_player.stop()
def undo(self):
if self.was_playing and self.prev_track:
self.music_player.play(self.prev_track)
# Macro Command (Composite Command)
class MacroCommand(Command):
def __init__(self, commands):
self.commands = commands
def execute(self):
for command in self.commands:
command.execute()
def undo(self):
# Undo in reverse order
for command in reversed(self.commands):
command.undo()
# Invoker
class SmartHomeController:
def __init__(self):
self.command = None
self.history = []
self.scheduled_commands = {}
def set_command(self, command):
self.command = command
def press_button(self):
if self.command:
self.command.execute()
self.history.append(self.command)
def press_undo_button(self):
if self.history:
last_command = self.history.pop()
last_command.undo()
def schedule_command(self, command, delay_seconds):
execution_time = time.time() + delay_seconds
self.scheduled_commands[execution_time] = command
print(f"Command scheduled to execute in {delay_seconds} seconds")
def check_scheduled_commands(self):
current_time = time.time()
commands_to_execute = []
times_to_remove = []
for execution_time, command in self.scheduled_commands.items():
if current_time >= execution_time:
commands_to_execute.append(command)
times_to_remove.append(execution_time)
for command in commands_to_execute:
command.execute()
self.history.append(command)
for time_to_remove in times_to_remove:
del self.scheduled_commands[time_to_remove]
# Client code
if __name__ == "__main__":
# Create receivers
living_room_light = Light("Living Room")
kitchen_light = Light("Kitchen")
bedroom_light = Light("Bedroom")
living_room_thermostat = Thermostat("Living Room")
music_player = MusicPlayer()
# Create commands
living_room_light_on = LightOnCommand(living_room_light)
living_room_light_off = LightOffCommand(living_room_light)
kitchen_light_on = LightOnCommand(kitchen_light)
kitchen_light_off = LightOffCommand(kitchen_light)
bedroom_light_on = LightOnCommand(bedroom_light)
bedroom_light_off = LightOffCommand(bedroom_light)
dim_living_room = DimLightCommand(living_room_light, 30)
set_thermostat = SetThermostatCommand(living_room_thermostat, 70)
play_jazz = PlayMusicCommand(music_player, "Jazz Playlist")
stop_music = StopMusicCommand(music_player)
# Create macro commands
evening_mode = MacroCommand([
dim_living_room,
set_thermostat,
play_jazz
])
all_lights_off = MacroCommand([
living_room_light_off,
kitchen_light_off,
bedroom_light_off
])
# Create invoker
smart_home = SmartHomeController()
# Execute commands
print("--- Setting Evening Mode ---")
smart_home.set_command(evening_mode)
smart_home.press_button()
print("\n--- Turning Kitchen Light On ---")
smart_home.set_command(kitchen_light_on)
smart_home.press_button()
print("\n--- Scheduling All Lights Off in 5 seconds ---")
smart_home.schedule_command(all_lights_off, 5)
# Simulate time passing
print("\nWaiting for scheduled command...")
time.sleep(5)
smart_home.check_scheduled_commands()
print("\n--- Undoing Last Command ---")
smart_home.press_undo_button()
How Command Pattern Handles OOP Principles
The Command pattern exemplifies several key OOP principles:
1. Encapsulation
- Commands encapsulate a request as an object
- All details about the operation are contained within the command object
- The invoker doesn’t need to know how the request is handled
2. Polymorphism
- Different commands implement the same interface
- The invoker can work with any command without knowing its concrete class
- New commands can be added without changing existing code
3. Single Responsibility Principle
- Each class has a single responsibility:
- Command: Knows how to perform an action
- Receiver: Knows how to execute the operations
- Invoker: Knows when to execute the command
- Client: Knows which commands to create
4. Open/Closed Principle
- The system is open for extension (new commands can be added)
- But closed for modification (existing code doesn’t need to change)
5. Dependency Inversion
- High-level modules (invoker) depend on abstractions (command interface)
- Low-level modules (concrete commands) also depend on abstractions
- This reduces coupling between components
Pain Points Without the Command Pattern
Without the Command pattern, you might encounter these issues:
1. Tight Coupling
Without Command pattern:
class RemoteControl:
def __init__(self, light, thermostat, music_player):
self.light = light
self.thermostat = thermostat
self.music_player = music_player
def turn_on_light(self):
self.light.turn_on()
def set_temperature(self, temp):
self.thermostat.set_temperature(temp)
def play_music(self, track):
self.music_player.play(track)
Problems: - The RemoteControl is tightly coupled to specific devices - Adding a new device requires modifying the RemoteControl class - The RemoteControl needs to know how to operate each device
2. Lack of Extensibility
- Adding new operations requires modifying existing code
- Can’t easily add features like undo/redo
- Can’t compose operations into macros
3. Difficult to Implement Advanced Features
Without Command pattern, these features become complex: - Queueing operations - Scheduling operations for later execution - Logging operations for audit purposes - Supporting transactional behavior - Implementing undo/redo functionality
4. Code Duplication
- Similar operation logic may be duplicated across different controllers
- Changes to operation logic require updates in multiple places
5. Testing Challenges
- Hard to test operations in isolation
- Can’t mock or substitute operations easily
- Difficult to verify operation sequences
When to Use the Command Pattern
The Command pattern is particularly useful when:
- You need to parameterize objects with operations
- You want to queue, specify, or execute requests at different times
- You need to support undo/redo operations
- You want to structure a system around high-level operations
- You need to implement callback functionality
- You want to create composite commands (macros)
Conclusion
The Command Design Pattern is a powerful tool in your OOP toolkit. It helps you decouple objects that invoke operations from objects that perform these operations. This separation provides flexibility, extensibility, and maintainability to your code.
By encapsulating requests as objects, you can: - Pass commands as method arguments - Store commands in data structures - Schedule command execution - Support undoable operations - Compose simple commands into complex ones
Understanding and applying the Command pattern will help you write more maintainable, flexible, and robust code.