3 min read

Replacing Long If-Else Chains With the Strategy Pattern

Long if-else chains and switch statements are a code smell. They violate the Open/Closed Principle — adding a new case requires modifying existing code. The Strategy Pattern solves this by encapsulating each algorithm in its own class and making them interchangeable.

Key sources: "Design Patterns" by Gamma, Helm, Johnson, Vlissides (Gang of Four), "Clean Code" by Robert C. Martin.


The Problem

Consider an e-commerce system that calculates shipping costs. Different carriers have different pricing algorithms:

def calculate_shipping(carrier, weight, distance):
    if carrier == "UPS":
        return weight * 0.5 + distance * 0.1
    elif carrier == "FedEx":
        return (weight * 0.3 + distance * 0.15) * 1.1
    elif carrier == "DHL":
        return 20 if weight < 5 else 30 + (weight - 5) * 2
    elif carrier == "USPS":
        return weight * 0.2 + distance * 0.05
    # Adding a new carrier requires editing this function

This code has several problems:

  • Violates Open/Closed Principle: Adding a carrier requires editing the function
  • Violates Single Responsibility: The function knows about every carrier's pricing
  • Hard to test: Testing all branches requires a single large test file
  • Brittle: A change to one carrier's logic risks breaking others

The Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm varies independently from clients that use it.

from abc import ABC, abstractmethod

class ShippingStrategy(ABC):
    @abstractmethod
    def calculate(self, weight: float, distance: float) -> float:
        pass

class UPSStrategy(ShippingStrategy):
    def calculate(self, weight, distance):
        return weight * 0.5 + distance * 0.1

class FedExStrategy(ShippingStrategy):
    def calculate(self, weight, distance):
        return (weight * 0.3 + distance * 0.15) * 1.1

class DHLStrategy(ShippingStrategy):
    def calculate(self, weight, distance):
        return 20.0 if weight < 5 else 30.0 + (weight - 5) * 2

class USPSStrategy(ShippingStrategy):
    def calculate(self, weight, distance):
        return weight * 0.2 + distance * 0.05

Using the Strategies

The client code no longer needs if-else chains. It selects a strategy and delegates:

class ShippingCalculator:
    def __init__(self):
        self.strategies = {
            "UPS": UPSStrategy(),
            "FedEx": FedExStrategy(),
            "DHL": DHLStrategy(),
            "USPS": USPSStrategy(),
        }

    def register_strategy(self, name, strategy):
        self.strategies[name] = strategy

    def calculate(self, carrier, weight, distance):
        strategy = self.strategies.get(carrier)
        if not strategy:
            raise ValueError(f"Unknown carrier: {carrier}")
        return strategy.calculate(weight, distance)

Adding a new carrier:

class AmazonShippingStrategy(ShippingStrategy):
    def calculate(self, weight, distance):
        return weight * 0.4 + distance * 0.08

calculator.register_strategy("Amazon", AmazonShippingStrategy())

No existing code changes. No risk of breaking other carriers.


Real-World Examples

Payment Processing

Payment gateways are a classic Strategy Pattern use case:

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

class CreditCardStrategy(PaymentStrategy):
    def pay(self, amount):
        # Process credit card payment
        return gateway.charge(self.card_number, amount)

class PayPalStrategy(PaymentStrategy):
    def pay(self, amount):
        # Process PayPal payment
        return paypal_api.execute_payment(self.email, amount)

class CryptoStrategy(PaymentStrategy):
    def pay(self, amount):
        # Process cryptocurrency payment
        return blockchain.send_transaction(self.wallet, amount)

Authentication

Different authentication methods can use the Strategy Pattern:

class AuthStrategy(ABC):
    @abstractmethod
    def authenticate(self, credentials) -> User:
        pass

class OAuthStrategy(AuthStrategy):
    def authenticate(self, credentials):
        return oauth_service.validate(credentials.token)

class PasswordStrategy(AuthStrategy):
    def authenticate(self, credentials):
        return db.verify_password(credentials.email, credentials.password)

class SSOStrategy(AuthStrategy):
    def authenticate(self, credentials):
        return saml_service.assert_identity(credentials.saml_response)

Data Export

Exporting data in different formats:

class ExportStrategy(ABC):
    @abstractmethod
    def export(self, data) -> str:
        pass

class JSONExportStrategy(ExportStrategy):
    def export(self, data):
        return json.dumps(data, indent=2)

class CSVExportStrategy(ExportStrategy):
    def export(self, data):
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow(data.keys())
        writer.writerow(data.values())
        return output.getvalue()

class XMLExportStrategy(ExportStrategy):
    def export(self, data):
        # Convert to XML
        pass

When to Use the Strategy Pattern

| Use Strategy Pattern When | Do Not Use When | |--------------------------|-----------------| | Multiple related classes differ only in behavior | The algorithm is simple and unlikely to change | | You need different variants of an algorithm | There are only 2-3 branches | | You want to avoid conditionals | The algorithms share significant state | | You need to add new variants frequently | Performance is critical and indirection matters |


When to Use Functions Instead

In languages with first-class functions, you do not always need a full Strategy class hierarchy. Simple cases can use function references:

strategies = {
    "UPS": lambda w, d: w * 0.5 + d * 0.1,
    "FedEx": lambda w, d: (w * 0.3 + d * 0.15) * 1.1,
    "DHL": lambda w, d: 20.0 if w < 5 else 30.0 + (w - 5) * 2,
    "USPS": lambda w, d: w * 0.2 + d * 0.05,
}

This is simpler for small strategies. Use the full class-based pattern when strategies are complex or share interfaces.


Key Takeaways

  1. Long if-else chains violate the Open/Closed Principle — adding new cases requires modifying existing code.
  2. The Strategy Pattern encapsulates each algorithm in its own class with a common interface.
  3. Clients select a strategy and delegate — no conditionals needed.
  4. New strategies can be added without changing existing code.
  5. Real-world use cases include payment processing, authentication, and data export.
  6. For simple cases, first-class functions can replace the pattern without the class overhead.

Design principle: Favor composition over inheritance. Strategy lets you change behavior by composing with different strategy objects rather than subclassing.