Table of Contents
- Introduction to Design Patterns
- What is the Command Pattern?
- 2.1 Intent
- 2.2 Structure
- 2.3 Python Example: Simple Remote Control
- What is the Strategy Pattern?
- 3.1 Intent
- 3.2 Structure
- 3.3 Python Example: Payment Processing
- Key Differences Between Command and Strategy
- When to Use the Command Pattern
- When to Use the Strategy Pattern
- Real-World Examples in Python
- 7.1 Command Pattern: Text Editor Undo/Redo
- 7.2 Strategy Pattern: Dynamic Sorting Algorithms
- Common Pitfalls and Best Practices
- Conclusion
- References
Introduction to Design Patterns
Behavioral design patterns focus on how objects interact and communicate. They help define clear protocols for collaboration between objects, ensuring flexibility in how responsibilities are delegated.
Two of the most widely used behavioral patterns are Command and Strategy. While both encapsulate behavior, they serve distinct goals:
- Command encapsulates actions (e.g., “turn on a light” or “save a file”) to decouple the sender of a request from its receiver.
- Strategy encapsulates algorithms (e.g., “sort data with quicksort” or “pay via credit card”) to enable dynamic switching between related algorithms.
What is the Command Pattern?
Intent
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.
In simpler terms: It turns a “command” (e.g., “copy,” “paste”) into a standalone object that can be stored, passed around, or executed later.
Structure
The Command pattern typically involves four components:
| Component | Role |
|---|---|
| Command | An interface/abstract class defining a execute() method (and optionally undo()). |
| Concrete Command | Implements execute() by invoking operations on a Receiver. Holds a reference to the Receiver. |
| Receiver | The object that performs the actual work when execute() is called (e.g., a Light or File object). |
| Invoker | Asks the Command to carry out the request (e.g., a button, remote control, or scheduler). |
| Client | Creates Concrete Command objects and links them to Receivers. |
Python Example: Simple Remote Control
Let’s model a remote control (Invoker) that can trigger actions (Commands) on devices (Receivers like a Light or Stereo).
Step 1: Define the Command Interface
We’ll use an abstract base class (ABC) to enforce the execute() method:
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
pass
# Optional: For undo support
@abstractmethod
def undo(self):
pass
Step 2: Create Receivers
Receivers perform the actual work. For example:
class Light:
def on(self):
print("Light is ON")
def off(self):
print("Light is OFF")
class Stereo:
def on(self):
print("Stereo is ON")
def play_music(self):
print("Stereo is playing music")
def off(self):
print("Stereo is OFF")
Step 3: Implement Concrete Commands
Concrete Commands link Receivers to actions:
class LightOnCommand(Command):
def __init__(self, light: Light):
self.light = light # Reference to Receiver
def execute(self):
self.light.on()
def undo(self):
self.light.off() # Undo "on" by turning "off"
class StereoPlayCommand(Command):
def __init__(self, stereo: Stereo):
self.stereo = stereo
def execute(self):
self.stereo.on()
self.stereo.play_music()
def undo(self):
self.stereo.off()
Step 4: Create the Invoker (Remote Control)
The Invoker triggers the Command:
class RemoteControl:
def __init__(self):
self.command = None # Holds the current Command
def set_command(self, command: Command):
self.command = command
def press_button(self):
if self.command:
self.command.execute()
def press_undo(self):
if self.command:
self.command.undo()
Step 5: Client Code (Putting It All Together)
# Client creates Receivers
living_room_light = Light()
bedroom_stereo = Stereo()
# Client creates Concrete Commands and links them to Receivers
light_on_cmd = LightOnCommand(living_room_light)
stereo_play_cmd = StereoPlayCommand(bedroom_stereo)
# Invoker (Remote Control) uses Commands
remote = RemoteControl()
# Press light button
remote.set_command(light_on_cmd)
remote.press_button() # Output: "Light is ON"
remote.press_undo() # Output: "Light is OFF"
# Press stereo button
remote.set_command(stereo_play_cmd)
remote.press_button() # Output: "Stereo is ON" → "Stereo is playing music"
remote.press_undo() # Output: "Stereo is OFF"
Here, the RemoteControl (Invoker) doesn’t know how the light or stereo works—it only knows to call execute() on the Command. This decouples the Invoker from the Receiver.
What is the Strategy Pattern?
Intent
The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable. This allows the algorithm to vary independently from clients that use it.
In simpler terms: It lets you swap out “strategies” (e.g., payment methods, sorting algorithms) dynamically without changing the client code that uses them.
Structure
The Strategy pattern involves three core components:
| Component | Role |
|---|---|
| Strategy | An interface/abstract class defining a method for the algorithm (e.g., pay(amount) or sort(data)). |
| Concrete Strategy | Implements the Strategy interface with a specific algorithm (e.g., CreditCardPayment, PayPalPayment). |
| Context | Uses a Strategy object to delegate work. Maintains a reference to a Concrete Strategy and may allow switching strategies at runtime. |
Python Example: Payment Processing
Let’s model a ShoppingCart (Context) that can process payments using different strategies (e.g., credit card, PayPal).
Step 1: Define the Strategy Interface
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
Step 2: Implement Concrete Strategies
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via Credit Card (****-****-****-{self.card_number[-4:]})")
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via PayPal (Account: {self.email})")
Step 3: Create the Context (ShoppingCart)
class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self.items = []
self.payment_strategy = payment_strategy # Reference to Strategy
def add_item(self, item: str, price: float) -> None:
self.items.append((item, price))
def set_payment_strategy(self, payment_strategy: PaymentStrategy) -> None:
# Allow switching strategies dynamically
self.payment_strategy = payment_strategy
def checkout(self) -> None:
total = sum(price for _, price in self.items)
print(f"Total: ${total}")
self.payment_strategy.pay(total) # Delegate payment to Strategy
Step 4: Client Code
# Client creates Concrete Strategies
credit_card = CreditCardPayment(card_number="1234-5678-9012-3456", cvv="123")
paypal = PayPalPayment(email="[email protected]")
# Context (ShoppingCart) uses a Strategy
cart = ShoppingCart(payment_strategy=credit_card)
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 25.50)
# Checkout with credit card
cart.checkout() # Output: "Total: $1025.49" → "Paid $1025.49 via Credit Card (****-****-****-3456)"
# Switch to PayPal at runtime
cart.set_payment_strategy(paypal)
cart.checkout() # Output: "Total: $1025.49" → "Paid $1025.49 via PayPal (Account: [email protected])"
Here, ShoppingCart (Context) doesn’t care how payment is processed—it delegates to the PaymentStrategy. We can add new payment methods (e.g., Bitcoin) by adding a new Concrete Strategy without modifying ShoppingCart.
Key Differences Between Command and Strategy
While both patterns encapsulate behavior, their goals and use cases are distinct. Here’s a side-by-side comparison:
| Feature | Command Pattern | Strategy Pattern |
|---|---|---|
| Primary Intent | Encapsulate actions as objects (e.g., “turn on,” “save”). | Encapsulate algorithms as interchangeable strategies (e.g., payment methods, sorting). |
| Focus | What to do (action). | How to do it (algorithm). |
| State Management | May store state to support undo()/redo() (e.g., previous light state). | Typically stateless (algorithms are pure functions). |
| Receiver Dependency | Requires a Receiver to perform the actual work. | No Receiver—Concrete Strategies contain the algorithm logic. |
| Typical Use Cases | Undo/redo, queuing requests, logging actions, GUI buttons. | Dynamic algorithm switching, replacing conditional logic, varying business rules. |
| Output | Executes an action (may have side effects). | Computes a result (e.g., sorted data, payment confirmation). |
When to Use the Command Pattern
Use Command when:
- You need to parameterize objects with actions (e.g., a button that can trigger different commands).
- You need to queue, schedule, or log requests (e.g., task schedulers, transaction logs).
- You need to support undo/redo operations (e.g., text editors, graphic design tools).
- You want to decouple the sender of a request from the receiver (e.g., a remote control that works with any device).
When to Use the Strategy Pattern
Use Strategy when:
- You have multiple algorithms for a specific task (e.g., different sorting or compression algorithms).
- You need to switch algorithms dynamically at runtime (e.g., changing payment methods in an e-commerce app).
- You want to isolate business logic from algorithm implementation (e.g., keeping
ShoppingCartclean of payment details). - A class has multiple conditional statements that can be replaced with strategies (e.g.,
if payment_type == "credit_card": ... elif payment_type == "paypal": ...).
Real-World Examples in Python
Example 1: Command Pattern – Text Editor Undo/Redo
Most text editors (e.g., VS Code, Notepad++) use the Command pattern to support undo/redo. Each edit (insert, delete) is a Command object stored in a history stack.
from abc import ABC, abstractmethod
class TextEditorCommand(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class InsertCommand(TextEditorCommand):
def __init__(self, editor, text: str, position: int):
self.editor = editor # Receiver: TextEditor
self.text = text
self.position = position
def execute(self):
# Insert text at position and save state for undo
self.editor.insert(self.position, self.text)
self.old_text = self.editor.text # Save previous state
def undo(self):
# Restore previous state
self.editor.text = self.old_text
class TextEditor:
def __init__(self):
self.text = ""
self.history = [] # Stack of executed commands
def insert(self, position: int, text: str):
self.text = self.text[:position] + text + self.text[position:]
def undo(self):
if self.history:
cmd = self.history.pop()
cmd.undo()
# Usage
editor = TextEditor()
insert_cmd = InsertCommand(editor, "Hello", 0)
insert_cmd.execute() # editor.text = "Hello"
editor.history.append(insert_cmd)
insert_cmd = InsertCommand(editor, " World", 5)
insert_cmd.execute() # editor.text = "Hello World"
editor.history.append(insert_cmd)
editor.undo() # editor.text = "Hello" (undoes " World" insertion)
Example 2: Strategy Pattern – Dynamic Sorting
A data processing tool might let users choose between sorting algorithms (bubble sort, quicksort) based on input size.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list:
pass
class BubbleSort(SortStrategy):
def sort(self, data: list) -> list:
data_copy = data.copy()
n = len(data_copy)
for i in range(n):
for j in range(0, n-i-1):
if data_copy[j] > data_copy[j+1]:
data_copy[j], data_copy[j+1] = data_copy[j+1], data_copy[j]
return data_copy
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return data.copy()
pivot = data[len(data)//2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class DataProcessor:
def __init__(self, sort_strategy: SortStrategy):
self.sort_strategy = sort_strategy
def set_strategy(self, sort_strategy: SortStrategy):
self.sort_strategy = sort_strategy
def process_data(self, data: list) -> list:
return self.sort_strategy.sort(data)
# Usage
data = [3, 1, 4, 1, 5, 9, 2, 6]
processor = DataProcessor(BubbleSort())
print(processor.process_data(data)) # Output: [1, 1, 2, 3, 4, 5, 6, 9] (Bubble Sort)
processor.set_strategy(QuickSort())
print(processor.process_data(data)) # Output: [1, 1, 2, 3, 4, 5, 6, 9] (Quick Sort)
Common Pitfalls and Best Practices
Pitfalls
- Over-engineering: Don’t use Command/Strategy if a simple function or conditional statement suffices. For example, a single payment method may not need Strategy.
- Command: Forgetting to handle
undo()correctly (e.g., not storing the previous state of the Receiver). - Strategy: Creating too many tiny strategies that bloat the codebase (e.g., 10+ payment strategies with minimal differences).
Best Practices
- Command: Keep commands focused on a single action. Use composite commands for complex actions (e.g., a
MacroCommandthat runs multiple commands). - Strategy: Keep strategies small and single-purpose. Use ABCs to enforce strategy interfaces.
- Both Patterns: Prefer composition over inheritance. Use dependency injection to pass commands/strategies to clients.
Conclusion
Command and Strategy are powerful behavioral patterns, but they solve distinct problems:
- Command is for encapsulating actions (e.g., undo/redo, queuing requests).
- Strategy is for encapsulating interchangeable algorithms (e.g., dynamic payment methods, sorting).
The key to choosing between them is to ask: Am I trying to parameterize actions (Command) or swap algorithms (Strategy)? By understanding their intents and structures, you can write more flexible, maintainable Python code.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Abstract Base Classes (ABCs)
- Real Python: Design Patterns in Python
- Refactoring Guru: Command Pattern
- Refactoring Guru: Strategy Pattern