Strategy Pattern: Interchangeable Algorithms
Learn: Strategy Pattern: Interchangeable Algorithms
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Strategy Pattern: Interchangeable Algorithms
Problem
Applications often need to support multiple algorithms for the same task, with the choice depending on runtime conditions. Without proper design, this leads to:
- Massive conditional logic scattered throughout the codebase
- Tight coupling between algorithm selection and implementation
- Difficult maintenance when adding new algorithms
- Code duplication across similar algorithm implementations
- Poor testability due to interdependencies
Example: A payment system supporting credit cards, PayPal, and cryptocurrency. Hardcoding all payment logic with if-else statements creates unmaintainable spaghetti code.
Solution
The Strategy Pattern encapsulates algorithms into separate, interchangeable classes that implement a common interface. This allows:
- Runtime selection of algorithms without modifying client code
- Loose coupling between algorithm selection and implementation
- Easy extension by adding new strategy classes
- Improved testability through dependency injection
- Single Responsibility Principle compliance
Core Concept: Define a family of algorithms, encapsulate each one, and make them interchangeable.
Code Implementation
1. Basic Payment Processing System
from abc import ABC, abstractmethod
from typing import Dict, Any
# Strategy Interface
class PaymentStrategy(ABC):
"""Abstract base class defining the payment algorithm interface"""
@abstractmethod
def pay(self, amount: float) -> bool:
"""Process payment and return success status"""
pass
@abstractmethod
def validate(self) -> bool:
"""Validate payment method before processing"""
pass
# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
"""Credit card payment implementation"""
def __init__(self, card_number: str, cvv: str, expiry: str):
self.card_number = card_number
self.cvv = cvv
self.expiry = expiry
def validate(self) -> bool:
return (len(self.card_number) == 16 and
len(self.cvv) == 3 and
self.expiry)
def pay(self, amount: float) -> bool:
if not self.validate():
print("❌ Invalid credit card details")
return False
print(f"💳 Processing ${amount} via Credit Card")
print(f" Card: ****{self.card_number[-4:]}")
return True
class PayPalPayment(PaymentStrategy):
"""PayPal payment implementation"""
def __init__(self, email: str, password: str):
self.email = email
self.password = password
def validate(self) -> bool:
return "@" in self.email and len(self.password) >= 6
def pay(self, amount: float) -> bool:
if not self.validate():
print("❌ Invalid PayPal credentials")
return False
print(f"🅿️ Processing ${amount} via PayPal")
print(f" Account: {self.email}")
return True
class CryptocurrencyPayment(PaymentStrategy):
"""Cryptocurrency payment implementation"""
def __init__(self, wallet_address: str, coin_type: str):
self.wallet_address = wallet_address
self.coin_type = coin_type
def validate(self) -> bool:
return len(self.wallet_address) >= 26 and self.coin_type in ["BTC", "ETH"]
def pay(self, amount: float) -> bool:
if not self.validate():
print("❌ Invalid wallet address")
return False
print(f"₿ Processing ${amount} via {self.coin_type}")
print(f" Wallet: {self.wallet_address[:10]}...")
return True
# Context Class
class PaymentProcessor:
"""Handles payment processing with interchangeable strategies"""
def __init__(self, strategy: PaymentStrategy = None):
self._strategy = strategy
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
"""Change payment strategy at runtime"""
self._strategy = strategy
def process_payment(self, amount: float) -> bool:
"""Execute payment using current strategy"""
if not self._strategy:
print("❌ No payment strategy set")
return False
return self._strategy.pay(amount)
def get_strategy_info(self) -> str:
"""Return information about current strategy"""
return self._strategy.__class__.__name__ if self._strategy else "None"
# Usage Example
if __name__ == "__main__":
processor = PaymentProcessor()
# Scenario 1: Credit Card Payment
print("=== Scenario 1: Credit Card ===")
cc_strategy = CreditCardPayment("1234567890123456", "123", "12/25")
processor.set_payment_strategy(cc_strategy)
processor.process_payment(99.99)
# Scenario 2: Switch to PayPal
print("\n=== Scenario 2: PayPal ===")
paypal_strategy = PayPalPayment("user@example.com", "secure_pass")
processor.set_payment_strategy(paypal_strategy)
processor.process_payment(49.99)
# Scenario 3: Switch to Cryptocurrency
print("\n=== Scenario 3: Cryptocurrency ===")
crypto_strategy = CryptocurrencyPayment(
"1A1z7agoat2YLZW51Yz8z7c8GV2r2SgNjX",
"BTC"
)
processor.set_payment_strategy(crypto_strategy)
processor.process_payment(0.0025)
2. Advanced: Sorting Algorithm Selection
from typing import List, Callable
class SortingStrategy(ABC):
"""Abstract sorting strategy"""
@abstractmethod
def sort(self, data: List[int]) -> List[int]:
pass
@abstractmethod
def complexity(self) -> str:
pass
class QuickSort(SortingStrategy):
"""Quick sort implementation"""
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)
def complexity(self) -> str:
return "O(n log n) average, O(n²) worst"
class MergeSort(SortingStrategy):
"""Merge sort implementation"""
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: List[int], right: List[int]) -> List[int]:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
return result + left[i:] + right[j:]
def complexity(self) -> str:
return "O(n log n) guaranteed"
class BubbleSort(SortingStrategy):
"""Bubble sort implementation"""
def sort(self, data: List[int]) -> List[int]:
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
def complexity(self) -> str:
return "O(n²)"
class DataSorter:
"""Context for sorting operations"""
def __init__(self, strategy: SortingStrategy = None):
self._strategy = strategy
def set_strategy(self, strategy: SortingStrategy) -> None:
self._strategy = strategy
def sort(self, data: List[int]) -> List[int]:
if not self._strategy:
raise ValueError("No sorting strategy set")
return self._strategy.sort(data)
def get_complexity(self) -> str:
if not self._strategy:
return "Unknown"
return self._strategy.complexity()
# Usage
if __name__ == "__main__":
data = [64, 34, 25, 12, 22, 11, 90]
sorter = DataSorter()
for strategy_class in [QuickSort, MergeSort, BubbleSort]:
sorter.set_strategy(strategy_class())
print(f"\n{strategy_class.__name__}:")
print(f" Complexity: {sorter.get_complexity()}")
print(f" Result: {sorter.sort(data)}")
3. Real-World: Compression Strategy
import json
from datetime import datetime
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data: str) -> bytes:
pass
@abstractmethod
def decompress(self, data: bytes) -> str:
pass
class GZipCompression(CompressionStrategy):
"""GZip compression"""
import gzip
def compress(self, data: str) -> bytes:
return self.gzip.compress(data.encode())
def decompress(self, data: bytes) -> str:
return self.gzip.decompress(data).decode()
class NoCompression(CompressionStrategy):
"""No compression - baseline"""
def compress(self, data: str) -> bytes:
return data.encode()
def decompress(self, data: bytes) -> str:
return data.decode()
class FileArchiver:
"""Manages file compression with runtime strategy selection"""
def __init__(self, strategy: CompressionStrategy):
self._strategy = strategy
def set_compression(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
def save_file(self, filename: str, content: str) -> Dict[str, Any]:
compressed = self._strategy.compress(content)
return {
"filename": filename,
"original_size": len(content),
"compressed_size": len(compressed),
"compression_ratio": f"{(1 - len(compressed)/len(content)) * 100:.1f}%",
"timestamp": datetime.now().isoformat()
}
# Usage
if __name__ == "__main__":
large_data = json.dumps({"data": [i for i in range(1000)]})
archiver = FileArchiver(NoCompression())
print("No Compression:", archiver.save_file("data.json", large_data))
archiver.set_compression(GZipCompression())
print("GZip Compression:", archiver.save_file("data.json.gz", large_data))
Tips & Best Practices
✅ Do's
- Use when algorithms vary - Multiple implementations of same behavior
- Encapsulate completely - Hide algorithm details from clients
- Inject strategies - Use dependency injection for flexibility
- Document complexity - Include time/space complexity information
- Provide factory methods - Simplify strategy creation
- Use composition - Combine strategies for complex behaviors
❌ Don'ts
- Don't overuse - Simple if-else is fine for 2-3 options
- Don't expose internals - Keep algorithm details private
- Don't share state - Make strategies stateless when possible
- Don't hardcode selection - Use configuration or parameters
- Don't create strategy per instance - Reuse immutable strategies
🎯 When to Use
- Payment processing - Multiple payment methods
- Sorting/searching - Different algorithms for different data
- Compression - Various compression techniques
- Validation - Different validation rules
- Caching - Multiple cache strategies
- Logging - Different output formats
🔧 Common Patterns
# Factory Pattern + Strategy
class StrategyFactory:
_strategies = {
"quick": QuickSort,
"merge": MergeSort,
"bubble": BubbleSort
}
@staticmethod
def create(name: str) -> SortingStrategy:
return StrategyFactory._strategies[name]()
# Configuration-based selection
config = {"payment_method": "paypal"}
strategy = PaymentFactory.create(config["payment_method"])
Summary
| Aspect | Benefit |
| Flexibility | Switch algorithms at runtime |
| Maintainability | Each algorithm isolated in own class |
| Testability | Mock strategies easily |
| Extensibility | Add new algorithms without modifying existing code |
| Clarity | Intent explicit through strategy classes |
The Strategy Pattern transforms rigid conditional logic into flexible, maintainable, and testable code by treating algorithms as first-class objects.