py4u guide

Python Design Patterns: Command vs. Strategy – When to Use Which

Design patterns are proven solutions to common software design problems. They help improve code readability, maintainability, and scalability by providing standardized approaches to recurring challenges. Among the 23 classic "Gang of Four" (GoF) design patterns, **Command** and **Strategy** are both behavioral patterns—but they solve distinct problems. Beginners and even intermediate developers often confuse these two patterns due to their superficial similarities (e.g., encapsulating behavior). However, their intents, structures, and use cases differ significantly. This blog post will demystify Command and Strategy patterns, explore their differences, and guide you on when to use each in Python.

Table of Contents

  1. Introduction to Design Patterns
  2. What is the Command Pattern?
    • 2.1 Intent
    • 2.2 Structure
    • 2.3 Python Example: Simple Remote Control
  3. What is the Strategy Pattern?
    • 3.1 Intent
    • 3.2 Structure
    • 3.3 Python Example: Payment Processing
  4. Key Differences Between Command and Strategy
  5. When to Use the Command Pattern
  6. When to Use the Strategy Pattern
  7. Real-World Examples in Python
    • 7.1 Command Pattern: Text Editor Undo/Redo
    • 7.2 Strategy Pattern: Dynamic Sorting Algorithms
  8. Common Pitfalls and Best Practices
  9. Conclusion
  10. 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:

ComponentRole
CommandAn interface/abstract class defining a execute() method (and optionally undo()).
Concrete CommandImplements execute() by invoking operations on a Receiver. Holds a reference to the Receiver.
ReceiverThe object that performs the actual work when execute() is called (e.g., a Light or File object).
InvokerAsks the Command to carry out the request (e.g., a button, remote control, or scheduler).
ClientCreates 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:

ComponentRole
StrategyAn interface/abstract class defining a method for the algorithm (e.g., pay(amount) or sort(data)).
Concrete StrategyImplements the Strategy interface with a specific algorithm (e.g., CreditCardPayment, PayPalPayment).
ContextUses 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:

FeatureCommand PatternStrategy Pattern
Primary IntentEncapsulate actions as objects (e.g., “turn on,” “save”).Encapsulate algorithms as interchangeable strategies (e.g., payment methods, sorting).
FocusWhat to do (action).How to do it (algorithm).
State ManagementMay store state to support undo()/redo() (e.g., previous light state).Typically stateless (algorithms are pure functions).
Receiver DependencyRequires a Receiver to perform the actual work.No Receiver—Concrete Strategies contain the algorithm logic.
Typical Use CasesUndo/redo, queuing requests, logging actions, GUI buttons.Dynamic algorithm switching, replacing conditional logic, varying business rules.
OutputExecutes 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 ShoppingCart clean 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 MacroCommand that 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