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:

python 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.

```python 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:

```python 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:

```python 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:

```python 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:

```python 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:

```python 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:

python 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.