Python Strategy Pattern

The Strategy Pattern is a behavioral design pattern that allows you to choose an algorithm or behavior at runtime. Simply put, the Strategy Pattern encapsulates different algorithms into independent classes, allowing them to replace each other.

Core Idea

Imagine you go to a restaurant to order food: you can choose different payment methods - cash, credit card, mobile payment, etc. No matter which payment method you choose, you can eventually complete the payment, but the specific payment process is different. This is a real-world example of the Strategy Pattern.

Design Principles

The Strategy Pattern follows the following important principles:

  • Open-Closed Principle: Open for extension, closed for modification.
  • Single Responsibility Principle: Each strategy class is responsible for only one algorithm.
  • Dependency Inversion Principle: Depend on abstractions, not on concrete implementations.

Strategy Pattern Structure

Let's understand the composition of the Strategy Pattern through a UML class diagram:

Component Description

Context

  • Maintains a reference to a strategy object.
  • Can dynamically switch strategies.
  • Delegates work to the strategy object.

Strategy (Strategy Interface)

  • Defines the common interface for all supported algorithms.
  • Declares the method for executing the algorithm.

ConcreteStrategy

  • Implements the strategy interface.
  • Provides concrete algorithm implementation.

Basic Syntax and Implementation

Strategy Interface Definition

In Python, we can use an abstract base class (ABC) to define the strategy interface:

Example

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """Payment Strategy Abstract Base Class"""
   
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """Payment Method"""
        pass

Concrete Strategy Implementation

Example

class CreditCardPayment(PaymentStrategy):
    """Credit Card Payment Strategy"""
   
    def __init__(self, card_number: str, expiry_date: str, cvv: str):
        self.card_number = card_number
        self.expiry_date = expiry_date
        self.cvv = cvv
   
    def pay(self, amount: float) -> bool:
        print(f"Pay {amount} yuan with credit card")
        print(f"Card Number: {self.card_number}")
        # Actual payment logic should be here
        return True

class AlipayPayment(PaymentStrategy):
    """Alipay Payment Strategy"""
   
    def __init__(self, account: str):
        self.account = account
   
    def pay(self, amount: float) -> bool:
        print(f"Pay {amount} yuan with Alipay")
        print(f"Alipay Account: {self.account}")
        # Actual payment logic should be here
        return True

class WechatPayment(PaymentStrategy):
    """WeChat Pay Strategy"""
   
    def __init__(self, openid: str):
        self.openid = openid
   
    def pay(self, amount: float) -> bool:
        print(f"Pay {amount} yuan with WeChat Pay")
        print(f"WeChat OpenID: {self.openid}")
        # Actual payment logic should be here
        return True

Context Class Implementation

Example

class PaymentContext:
    """Payment Context Class"""
   
    def __init__(self, strategy: PaymentStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: PaymentStrategy):
        """Set Payment Strategy"""
        self._strategy = strategy
   
    def execute_payment(self, amount: float) -> bool:
        """Execute Payment"""
        if not self._strategy:
            raise ValueError("No payment strategy set")
       
        return self._strategy.pay(amount)

Complete Example: E-commerce Payment System

Let's demonstrate the practical application of the Strategy Pattern through a complete e-commerce payment system:

Example

from abc import ABC, abstractmethod
from typing import Dict, Any

# Strategy interface
class DiscountStrategy(ABC):
    """Discount Strategy Interface"""
   
    @abstractmethod
    def calculate_discount(self, original_price: float) -> float:
        """Calculate price after discount"""
        pass

# Concrete strategy implementations
class NoDiscountStrategy(DiscountStrategy):
    """No Discount Strategy"""
   
    def calculate_discount(self, original_price: float) -> float:
        return original_price

class PercentageDiscountStrategy(DiscountStrategy):
    """Percentage Discount Strategy"""
   
    def __init__(self, percentage: float):
        if not 0 <= percentage <= 100:
            raise ValueError("Discount percentage must be between 0-100")
        self.percentage = percentage
   
    def calculate_discount(self, original_price: float) -> float:
        discount_amount = original_price * (self.percentage / 100)
        return original_price - discount_amount

class FixedAmountDiscountStrategy(DiscountStrategy):
    """Fixed amount discount strategy"""
   
    def __init__(self, discount_amount: float):
        if discount_amount < 0:
            raise ValueError("Discount amount cannot be negative")
        self.discount_amount = discount_amount
   
    def calculate_discount(self, original_price: float) -> float:
        return max(0, original_price - self.discount_amount)

class SeasonalDiscountStrategy(DiscountStrategy):
    """Seasonal discount strategy"""
   
    def __init__(self, base_discount: float, seasonal_multiplier: float):
        self.base_discount = base_discount
        self.seasonal_multiplier = seasonal_multiplier
   
    def calculate_discount(self, original_price: float) -> float:
        total_discount = self.base_discount * self.seasonal_multiplier
        return max(0, original_price - total_discount)

# Context class
class ShoppingCart:
    """Shopping cart class"""
   
    def __init__(self):
        self.items = []
        self._discount_strategy = NoDiscountStrategy()
   
    def add_item(self, item: str, price: float):
        """Add item"""
        self.items.append({"item": item, "price": price})
   
    def set_discount_strategy(self, strategy: DiscountStrategy):
        """Set discount strategy"""
        self._discount_strategy = strategy
   
    def calculate_total(self) -> float:
        """Calculate total price"""
        total = sum(item["price"] for item in self.items)
        return self._discount_strategy.calculate_discount(total)
   
    def display_cart(self):
        """Display cart contents"""
        print("Cart contents:")
        for item in self.items:
            print(f" - {item['item']}: {item['price']} yuan")
       
        original_total = sum(item["price"] for item in self.items)
        final_total = self.calculate_total()
       
        print(f"Original price: {original_total} yuan")
        print(f"Discounted price: {final_total} yuan")
       
        if original_total != final_total:
            discount = original_total - final_total
            print(f"Savings: {discount} yuan")

# Usage example
def main():
    # Create shopping cart
    cart = ShoppingCart()
   
    # Add items
    cart.add_item("Python programming book", 89.0)
    cart.add_item("Wireless mouse", 129.0)
    cart.add_item("Mechanical keyboard", 399.0)
   
    print("=== No discount ===")
    cart.set_discount_strategy(NoDiscountStrategy())
    cart.display_cart()
   
    print("\n"=== 20% off ===")
    cart.set_discount_strategy(PercentageDiscountStrategy(20))  # 20% off
    cart.display_cart()
   
    print("\n"=== Threshold discount (50 yuan off) ===")
    cart.set_discount_strategy(FixedAmountDiscountStrategy(50))
    cart.display_cart()
   
    print("\n"=== Seasonal discount ===")
    cart.set_discount_strategy(SeasonalDiscountStrategy(30, 1.5))  # Base discount 30, seasonal coefficient 1.5
    cart.display_cart()

if __name__ == "__main__":
    main()

Running the above code, you will see the following output:

=== 无折扣 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 617.0元

=== 8折优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 493.6元
节省: 123.4元

=== 满减优惠(减50元)===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 567.0元
节省: 50.0元

=== 季节性优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 572.0元
节省: 45.0元

Advanced Usage of the Strategy Pattern

1. Strategy Factory Pattern

Combine the factory pattern to manage the creation of strategies:

Example

class DiscountStrategyFactory:
    """Discount strategy factory"""
   
    @staticmethod
    def create_strategy(strategy_type: str, **kwargs) -> DiscountStrategy:
        """Create discount strategy"""
        strategies = {
            "no_discount": NoDiscountStrategy,
            "percentage": PercentageDiscountStrategy,
            "fixed_amount": FixedAmountDiscountStrategy,
            "seasonal": SeasonalDiscountStrategy
        }
       
        if strategy_type not in strategies:
            raise ValueError(f"Unsupported strategy type: {strategy_type}")
       
        return strategies[strategy_type](**kwargs)

# Use factory pattern
factory = DiscountStrategyFactory()

# Create different strategies
strategy1 = factory.create_strategy("percentage", percentage=15)  # 15% off
strategy2 = factory.create_strategy("fixed_amount", discount_amount=100)  # 100 yuan off

2. Dynamic Strategy Selection

Dynamically select strategies based on conditions:

Example

class DynamicDiscountSelector:
    """Dynamic discount selector"""
   
    @staticmethod
    def select_strategy(user_type: str, total_amount: float) -> DiscountStrategy:
        """Select strategy based on user type and total amount"""
        if user_type == "vip":
            if total_amount > 500:
                return PercentageDiscountStrategy(25)  # VIP: 25% off on orders of 500 or more
            else:
                return PercentageDiscountStrategy(15)  # VIP: 15% off
        elif user_type == "normal":
            if total_amount > 300:
                return FixedAmountDiscountStrategy(30)  # Regular users: 30 yuan off on orders of 300 or more
            else:
                return NoDiscountStrategy()
        else:
            return NoDiscountStrategy()

# Use dynamic selection
cart = ShoppingCart()
cart.add_item("Product A", 200)
cart.add_item("Product B", 150)

selector = DynamicDiscountSelector()
strategy = selector.select_strategy("vip", cart.calculate_total())
cart.set_discount_strategy(strategy)
cart.display_cart()

Advantages and Applicable Scenarios of the Strategy Pattern

Advantage Comparison

Feature Traditional Approach Strategy Pattern
Extensibility Requires modifying existing code Simply add a new strategy class
Maintainability High code coupling Separation of responsibilities, easy to maintain
Flexibility Difficult to switch algorithms at runtime Can dynamically switch strategies
Testability Difficult to test algorithms in isolation Each strategy can be tested independently

Applicable Scenarios

  1. Multiple algorithm variants: When you have several similar classes that differ only in certain behaviors.
  2. Avoid conditional statements: When you want to avoid using a large number of conditional statements (if-else or switch).
  3. Runtime algorithm selection: When you need to choose different algorithms at runtime.
  4. Algorithm encapsulation: When you want to isolate algorithm details from the clients that use the algorithm.

Inapplicable Scenarios

  1. Simple algorithms: If there are only one or two algorithms that rarely change, it may be over-engineering.
  2. Client needs to know strategy details: If the client must know the concrete implementation of the strategy.
  3. Too many strategies: When the number of strategy classes explodes, consider other patterns.

Best Practices and Considerations

Code Organization Suggestions

Example

# Recommended file structure
project/
├── strategies/
│   ├── __init__.py
│   ├── base_strategy.py      # Base strategy interface
│   ├── discount_strategies.py # Discount-related strategies
│   └── payment_strategies.py # Payment-related strategies
├── contexts/
│   ├── __init__.py
│   └── shopping_cart.py      # Context class
└── main.py

Error Handling

Example

class SafeDiscountStrategy(DiscountStrategy):
    """Discount strategy with error handling"""
   
    def __init__(self, base_strategy: DiscountStrategy, fallback_strategy: DiscountStrategy = None):
        self.base_strategy = base_strategy
        self.fallback_strategy = fallback_strategy or NoDiscountStrategy()
   
    def calculate_discount(self, original_price: float) -> float:
        try:
            return self.base_strategy.calculate_discount(original_price)
        except Exception as e:
            print(f"Discount calculation error: {e}, using fallback strategy")
            return self.fallback_strategy.calculate_discount(original_price)

Performance Considerations

For performance-sensitive scenarios, consider the following optimizations:

Example

from functools import lru_cache

class CachedDiscountStrategy(DiscountStrategy):
    """Discount strategy with caching"""
   
    def __init__(self, base_strategy: DiscountStrategy):
        self.base_strategy = base_strategy
   
    @lru_cache(maxsize=128)
    def calculate_discount(self, original_price: float) -> float:
        return self.base_strategy.calculate_discount(original_price)

Practical Exercises

Exercise 1: Sorting Strategy Implementation

Implement a sorter that supports multiple sorting algorithms:

Example

from abc import ABC, abstractmethod
from typing import List

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: List) -> List:
        pass

# TODO: Implement bubble sort strategy
class BubbleSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Your implementation code
        pass

# TODO: Implement quick sort strategy
class QuickSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Your implementation code
        pass

# TODO: Implement merge sort strategy
class MergeSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Your implementation code
        pass

class Sorter:
    def __init__(self, strategy: SortStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy
   
    def sort_data(self, data: List) -> List:
        if not self._strategy:
            raise ValueError("Sorting strategy not set")
        return self._strategy.sort(data)

# Test your implementation
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
result = sorter.sort_data(data)
print(f"Sorting result: {result}")

Exercise 2: File Compression Strategy

Design a file compressor that supports multiple compression formats:

Example

from abc import ABC, abstractmethod

class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, file_path: str) -> str:
        pass
   
    @abstractmethod
    def decompress(self, file_path: str) -> str:
        pass

# TODO: Implement ZIP compression strategy
class ZipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Your implementation code
        pass
   
    def decompress(self, file_path: str) -> str:
        # Your implementation code
        pass

# TODO: Implement GZIP compression strategy
class GzipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Your implementation code
        pass
   
    def decompress(self, file_path: str) -> str:
        # Your implementation code
        pass

class FileCompressor:
    def __init__(self, strategy: CompressionStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: CompressionStrategy):
        self._strategy = strategy
   
    def compress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("Compression strategy not set")
        return self._strategy.compress(file_path)
   
    def decompress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("Compression strategy not set")
        return self._strategy.decompress(file_path)

Summary

The Strategy pattern is a very practical design pattern in Python. By encapsulating algorithms into independent strategy classes, it provides good extensibility and flexibility. After studying this article, you should be able to:

  1. Understand the core concepts and applicable scenarios of the Strategy pattern
  2. Master the basic implementation methods of the Strategy pattern
  3. Apply the Strategy pattern appropriately in real projects
  4. Avoid common misuses of the Strategy pattern
Other Extensions