Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.


Problem

You have a class that needs to perform an algorithm, but the algorithm can vary based on context. You want to avoid hardcoding different algorithms in many conditional branches.

Solution

The Strategy pattern extracts algorithms into separate Strategy classes that implement a common interface. The context uses the appropriate strategy at runtime.


Implementation

from abc import ABC, abstractmethod
from typing import List

# Strategy interface
class FilterStrategy(ABC):
    @abstractmethod
    def should_remove(self, value: int) -> bool:
        pass

# Concrete Strategies
class RemoveNegativeStrategy(FilterStrategy):
    def should_remove(self, value: int) -> bool:
        return value < 0

class RemoveOddStrategy(FilterStrategy):
    def should_remove(self, value: int) -> bool:
        return abs(value) % 2 == 1

class RemoveEvenStrategy(FilterStrategy):
    def should_remove(self, value: int) -> bool:
        return value % 2 == 0

class RemoveMultiplesStrategy(FilterStrategy):
    def __init__(self, divisor: int):
        self.divisor = divisor

    def should_remove(self, value: int) -> bool:
        return value % self.divisor == 0

# Context
class ValueFilter:
    def __init__(self, strategy: FilterStrategy):
        self.strategy = strategy

    def set_strategy(self, strategy: FilterStrategy):
        self.strategy = strategy

    def filter(self, values: List[int]) -> List[int]:
        return [v for v in values if not self.strategy.should_remove(v)]

# Payment Strategy example
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def pay(self, amount: float) -> bool:
        print(f"Processing credit card payment of ${amount} with card {self.card_number[-4:]}")
        return True

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def pay(self, amount: float) -> bool:
        print(f"Processing PayPal payment of ${amount} to {self.email}")
        return True

class CryptocurrencyPayment(PaymentStrategy):
    def __init__(self, wallet_address: str):
        self.wallet_address = wallet_address

    def pay(self, amount: float) -> bool:
        print(f"Processing crypto payment of ${amount} to {self.wallet_address}")
        return True

class ShoppingCart:
    def __init__(self, payment_strategy: PaymentStrategy):
        self.items = []
        self.payment_strategy = payment_strategy

    def set_payment_strategy(self, strategy: PaymentStrategy):
        self.payment_strategy = strategy

    def add_item(self, price: float):
        self.items.append(price)

    def checkout(self) -> bool:
        total = sum(self.items)
        return self.payment_strategy.pay(total)

# Demo
if __name__ == "__main__":
    # Filter example
    print("=== Filter Strategy ===")
    values = [-7, -4, 0, 2, 5, 6, 9]

    filter_obj = ValueFilter(RemoveNegativeStrategy())
    print(f"Original: {values}")
    print(f"Remove negatives: {filter_obj.filter(values)}")

    filter_obj.set_strategy(RemoveOddStrategy())
    print(f"Remove odd: {filter_obj.filter(values)}")

    filter_obj.set_strategy(RemoveMultiplesStrategy(3))
    print(f"Remove multiples of 3: {filter_obj.filter(values)}\n")

    # Payment example
    print("=== Payment Strategy ===")
    cart = ShoppingCart(CreditCardPayment("1234-5678-9012-3456"))
    cart.add_item(29.99)
    cart.add_item(15.50)

    print("Using Credit Card:")
    cart.checkout()

    cart.set_payment_strategy(PayPalPayment("user@example.com"))
    print("\nUsing PayPal:")
    cart.checkout()

    cart.set_payment_strategy(CryptocurrencyPayment("0x1234..."))
    print("\nUsing Cryptocurrency:")
    cart.checkout()

Key Concepts

  • Strategy: Abstract algorithm interface
  • Concrete Strategy: Implementation of specific algorithm
  • Context: Uses a strategy to perform work
  • Runtime Selection: Strategy chosen at runtime, not compile time

When to Use

✅ Multiple algorithms for a task (sorting, filtering, payment)
✅ Want to avoid massive if-else chains
✅ Need to switch algorithms at runtime
✅ Different implementations for different contexts


Interview Q&A

Q1: What's the difference between Strategy and State patterns?

A: - Strategy: Client chooses which algorithm to use. All strategies are valid anytime. - State: Object's internal state determines which behavior to use. State transitions follow rules.

# Strategy: Client decides
calculator.set_strategy(AddStrategy())  # Client picks

# State: State machine decides
traffic_light.change()  # Transitions: RED → GREEN → YELLOW → RED

Q2: How would you implement a sorting strategy?

A:

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

class BubbleSortStrategy(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        # Bubble sort implementation
        arr = data.copy()
        for i in range(len(arr)):
            for j in range(len(arr) - 1 - i):
                if arr[j] > arr[j + 1]:
                    arr[j], arr[j + 1] = arr[j + 1], arr[j]
        return arr

class QuickSortStrategy(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        if len(data) <= 1:
            return data
        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 Sorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy

    def sort(self, data: List[int]) -> List[int]:
        return self.strategy.sort(data)

# Usage
sorter = Sorter(QuickSortStrategy())
print(sorter.sort([3, 1, 4, 1, 5]))  # [1, 1, 3, 4, 5]

Q3: How would you combine Strategy with Factory patterns?

A:

class StrategyFactory:
    @staticmethod
    def create_filter_strategy(strategy_name: str) -> FilterStrategy:
        strategies = {
            "negative": RemoveNegativeStrategy,
            "odd": RemoveOddStrategy,
            "even": RemoveEvenStrategy,
        }
        strategy_class = strategies.get(strategy_name)
        if strategy_class:
            return strategy_class()
        raise ValueError(f"Unknown strategy: {strategy_name}")

# Usage
strategy = StrategyFactory.create_filter_strategy("odd")
filter_obj = ValueFilter(strategy)

Q4: What happens if you need context-specific data in a strategy?

A: Pass context data to the strategy method:

class Strategy(ABC):
    @abstractmethod
    def execute(self, context_data: Dict[str, Any]) -> Any:
        pass

class ConcreteStrategy(Strategy):
    def execute(self, context_data: Dict[str, Any]) -> Any:
        user_level = context_data.get("user_level")
        price = context_data.get("price")

        if user_level == "premium":
            return price * 0.8  # 20% discount
        return price

Q5: How would you handle strategy-specific configuration?

A:

class Strategy(ABC):
    @abstractmethod
    def execute(self, data: Any) -> Any:
        pass

class ConfigurableStrategy(Strategy):
    def __init__(self, config: Dict[str, Any]):
        self.config = config

    def execute(self, data: Any) -> Any:
        threshold = self.config.get("threshold", 0)
        return [x for x in data if x > threshold]

# Usage
strategy = ConfigurableStrategy({"threshold": 5})

Trade-offs

Pros: Easy to add new algorithms, avoids large conditionals, runtime selection
Cons: Extra classes, memory overhead for simple algorithms, overkill if only 1-2 strategies


Real-World Examples

  • Compression algorithms (ZIP, RAR, 7Z)
  • Payment methods (Credit card, PayPal, Crypto)
  • Sorting algorithms (Quick sort, Merge sort, Bubble sort)
  • Caching strategies (LRU, LFU, FIFO)