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
- Long if-else chains violate the Open/Closed Principle — adding new cases requires modifying existing code.
- The Strategy Pattern encapsulates each algorithm in its own class with a common interface.
- Clients select a strategy and delegate — no conditionals needed.
- New strategies can be added without changing existing code.
- Real-world use cases include payment processing, authentication, and data export.
- 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.