The Strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it, promoting flexibility and maintainability in code design.
Core Concept
The Strategy pattern addresses the problem of having multiple ways to perform a task. Instead of using conditional statements to select different algorithms, the pattern extracts each algorithm into separate classes that implement a common interface. This approach follows the Open/Closed Principle - classes are open for extension but closed for modification.
Pattern Structure
┌─────────────────┐
│ Context │
│ │
│ - strategy │◄────────────────┐
│ + set_strategy │ │
│ + execute() │ │
└─────────────────┘ │
│
│
┌─────────────────┴──────────────┐
│ Strategy │
│ │
│ + execute(data) │
└─────────────────┬──────────────┘
│
┌────────────────┼─────────────────┐
│ │ │
┌──────────────▼───┐ ┌─────────▼───────┐ ┌─────────▼────────┐
│ ConcreteStrategyA│ │ConcreteStrategyB│ │ConcreteStrategyC │
│ │ │ │ │ │
│ + execute(data) │ │ + execute(data) │ │ + execute(data) │
└──────────────────┘ └─────────────────┘ └──────────────────┘
Key Components
Strategy Interface: Defines the common interface that all concrete strategies must implement.
Concrete Strategies: Implement different algorithms using the strategy interface.
Context: Maintains a reference to a strategy object and delegates algorithm execution to it.
Benefits
The Strategy pattern provides several advantages:
- Algorithm Independence: Algorithms can be developed and modified independently
- Runtime Flexibility: Strategy can be changed at runtime based on conditions
- Code Reusability: Strategies can be reused across different contexts
- Testability: Each strategy can be unit tested independently
- Maintainability: Adding new strategies doesn’t require modifying existing code
When to Use
Consider the Strategy pattern when:
- You have multiple ways to perform a task
- You want to avoid large conditional statements
- You need to switch algorithms at runtime
- You want to isolate algorithm implementation details
- Different variants of an algorithm are needed
Example 1: Payment Processing System
This example demonstrates how the Strategy pattern can handle different payment methods in an e-commerce system.In this payment processing example, each payment method (Credit Card, PayPal, Bank Transfer) is implemented as a separate strategy. The PaymentContext
class delegates the actual payment processing to the selected strategy, allowing the system to handle different payment methods seamlessly. New payment methods can be added by creating new strategy classes without modifying existing code.
from abc import ABC, abstractmethod
from typing import Dict, Any
from dataclasses import dataclass
# Strategy Interface
class PaymentStrategy(ABC):
"""Abstract base class defining the payment strategy interface."""
@abstractmethod
def process_payment(self, amount: float, payment_details: Dict[str, Any]) -> bool:
"""
Process a payment using the specific strategy.
Args:
amount: The payment amount
payment_details: Dictionary containing payment-specific details
Returns:
bool: True if payment successful, False otherwise
"""
pass
@abstractmethod
def validate_payment_details(self, payment_details: Dict[str, Any]) -> bool:
"""
Validate payment details for this strategy.
Args:
payment_details: Dictionary containing payment-specific details
Returns:
bool: True if details are valid, False otherwise
"""
pass
# Concrete Strategy 1: Credit Card Payment
class CreditCardPayment(PaymentStrategy):
"""Concrete strategy for processing credit card payments."""
def process_payment(self, amount: float, payment_details: Dict[str, Any]) -> bool:
"""Process credit card payment."""
if not self.validate_payment_details(payment_details):
print("Invalid credit card details")
return False
# Simulate credit card processing
print(f"Processing credit card payment of ${amount:.2f}")
print(f"Card Number: ****-****-****-{payment_details['card_number'][-4:]}")
print("Credit card payment successful!")
return True
def validate_payment_details(self, payment_details: Dict[str, Any]) -> bool:
"""Validate credit card details."""
required_fields = ['card_number', 'cvv', 'expiry_date', 'holder_name']
return all(field in payment_details for field in required_fields)
# Concrete Strategy 2: PayPal Payment
class PayPalPayment(PaymentStrategy):
"""Concrete strategy for processing PayPal payments."""
def process_payment(self, amount: float, payment_details: Dict[str, Any]) -> bool:
"""Process PayPal payment."""
if not self.validate_payment_details(payment_details):
print("Invalid PayPal details")
return False
# Simulate PayPal processing
print(f"Processing PayPal payment of ${amount:.2f}")
print(f"PayPal Email: {payment_details['email']}")
print("PayPal payment successful!")
return True
def validate_payment_details(self, payment_details: Dict[str, Any]) -> bool:
"""Validate PayPal details."""
return 'email' in payment_details and '@' in payment_details['email']
# Concrete Strategy 3: Bank Transfer Payment
class BankTransferPayment(PaymentStrategy):
"""Concrete strategy for processing bank transfer payments."""
def process_payment(self, amount: float, payment_details: Dict[str, Any]) -> bool:
"""Process bank transfer payment."""
if not self.validate_payment_details(payment_details):
print("Invalid bank transfer details")
return False
# Simulate bank transfer processing
print(f"Processing bank transfer of ${amount:.2f}")
print(f"Account Number: ****{payment_details['account_number'][-4:]}")
print("Bank transfer initiated successfully!")
return True
def validate_payment_details(self, payment_details: Dict[str, Any]) -> bool:
"""Validate bank transfer details."""
required_fields = ['account_number', 'routing_number', 'account_holder']
return all(field in payment_details for field in required_fields)
# Context Class
@dataclass
class PaymentContext:
"""Context class that uses payment strategies."""
def __init__(self):
self._strategy: PaymentStrategy = None
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
"""
Set the payment strategy to use.
Args:
strategy: The payment strategy to use
"""
self._strategy = strategy
def execute_payment(self, amount: float, payment_details: Dict[str, Any]) -> bool:
"""
Execute payment using the current strategy.
Args:
amount: Payment amount
payment_details: Payment-specific details
Returns:
bool: True if payment successful, False otherwise
"""
if self._strategy is None:
print("No payment strategy set!")
return False
print("=" * 50)
return self._strategy.process_payment(amount, payment_details)
# Example usage
if __name__ == "__main__":
# Create payment context
payment_processor = PaymentContext()
# Example 1: Credit Card Payment
credit_card_details = {
'card_number': '1234567890123456',
'cvv': '123',
'expiry_date': '12/25',
'holder_name': 'John Doe'
}
payment_processor.set_payment_strategy(CreditCardPayment())
payment_processor.execute_payment(99.99, credit_card_details)
# Example 2: PayPal Payment
paypal_details = {
'email': 'user@example.com'
}
payment_processor.set_payment_strategy(PayPalPayment())
payment_processor.execute_payment(49.99, paypal_details)
# Example 3: Bank Transfer Payment
bank_details = {
'account_number': '9876543210',
'routing_number': '123456789',
'account_holder': 'Jane Smith'
}
payment_processor.set_payment_strategy(BankTransferPayment())
payment_processor.execute_payment(199.99, bank_details)
Example 2: Data Compression System
This example shows how the Strategy pattern can be applied to different data compression algorithms.This compression example demonstrates how the Strategy pattern enables switching between different compression algorithms (GZIP, ZLIB, or no compression) at runtime. The DataCompressor
context class provides a consistent interface for compression operations while delegating the actual compression work to the selected strategy. The example also includes a comparison feature that evaluates all available strategies on the same data.
from abc import ABC, abstractmethod
import gzip
import zlib
from typing import bytes, Tuple
# Strategy Interface
class CompressionStrategy(ABC):
"""Abstract base class defining the compression strategy interface."""
@abstractmethod
def compress(self, data: bytes) -> bytes:
"""
Compress the input data.
Args:
data: Raw data to compress
Returns:
bytes: Compressed data
"""
pass
@abstractmethod
def decompress(self, compressed_data: bytes) -> bytes:
"""
Decompress the compressed data.
Args:
compressed_data: Compressed data to decompress
Returns:
bytes: Decompressed original data
"""
pass
@abstractmethod
def get_algorithm_name(self) -> str:
"""
Get the name of the compression algorithm.
Returns:
str: Algorithm name
"""
pass
# Concrete Strategy 1: GZIP Compression
class GZIPCompression(CompressionStrategy):
"""Concrete strategy for GZIP compression."""
def compress(self, data: bytes) -> bytes:
"""Compress data using GZIP algorithm."""
return gzip.compress(data)
def decompress(self, compressed_data: bytes) -> bytes:
"""Decompress GZIP compressed data."""
return gzip.decompress(compressed_data)
def get_algorithm_name(self) -> str:
"""Return algorithm name."""
return "GZIP"
# Concrete Strategy 2: ZLIB Compression
class ZLIBCompression(CompressionStrategy):
"""Concrete strategy for ZLIB compression."""
def compress(self, data: bytes) -> bytes:
"""Compress data using ZLIB algorithm."""
return zlib.compress(data)
def decompress(self, compressed_data: bytes) -> bytes:
"""Decompress ZLIB compressed data."""
return zlib.decompress(compressed_data)
def get_algorithm_name(self) -> str:
"""Return algorithm name."""
return "ZLIB"
# Concrete Strategy 3: No Compression (Pass-through)
class NoCompression(CompressionStrategy):
"""Concrete strategy for no compression (pass-through)."""
def compress(self, data: bytes) -> bytes:
"""Return data as-is (no compression)."""
return data
def decompress(self, compressed_data: bytes) -> bytes:
"""Return data as-is (no decompression)."""
return compressed_data
def get_algorithm_name(self) -> str:
"""Return algorithm name."""
return "NONE"
# Context Class
class DataCompressor:
"""Context class that uses compression strategies."""
def __init__(self, strategy: CompressionStrategy = None):
"""
Initialize with an optional compression strategy.
Args:
strategy: Initial compression strategy
"""
self._strategy = strategy or NoCompression()
def set_compression_strategy(self, strategy: CompressionStrategy) -> None:
"""
Set the compression strategy to use.
Args:
strategy: The compression strategy to use
"""
self._strategy = strategy
def compress_data(self, data: str) -> Tuple[bytes, dict]:
"""
Compress string data using the current strategy.
Args:
data: String data to compress
Returns:
Tuple containing compressed data and compression info
"""
# Convert string to bytes
raw_data = data.encode('utf-8')
# Compress using current strategy
compressed_data = self._strategy.compress(raw_data)
# Calculate compression statistics
original_size = len(raw_data)
compressed_size = len(compressed_data)
compression_ratio = (1 - compressed_size / original_size) * 100 if original_size > 0 else 0
stats = {
'algorithm': self._strategy.get_algorithm_name(),
'original_size': original_size,
'compressed_size': compressed_size,
'compression_ratio': compression_ratio
}
return compressed_data, stats
def decompress_data(self, compressed_data: bytes) -> str:
"""
Decompress data using the current strategy.
Args:
compressed_data: Compressed data to decompress
Returns:
str: Decompressed string data
"""
# Decompress using current strategy
raw_data = self._strategy.decompress(compressed_data)
# Convert bytes back to string
return raw_data.decode('utf-8')
def compare_strategies(self, data: str, strategies: list[CompressionStrategy]) -> None:
"""
Compare different compression strategies on the same data.
Args:
data: Data to compress
strategies: List of strategies to compare
"""
print("Compression Strategy Comparison")
print("=" * 60)
print(f"Original data size: {len(data.encode('utf-8'))} bytes")
print("-" * 60)
results = []
for strategy in strategies:
self.set_compression_strategy(strategy)
compressed_data, stats = self.compress_data(data)
results.append(stats)
print(f"Algorithm: {stats['algorithm']:<8} | "
f"Compressed: {stats['compressed_size']:<6} bytes | "
f"Ratio: {stats['compression_ratio']:<6.2f}%")
print("-" * 60)
# Find best compression
best_strategy = min(results, key=lambda x: x['compressed_size'])
print(f"Best compression: {best_strategy['algorithm']} "
f"({best_strategy['compression_ratio']:.2f}% reduction)")
# Example usage
if __name__ == "__main__":
# Create sample data to compress
sample_data = """
The Strategy pattern is a behavioral design pattern that defines a family of algorithms,
encapsulates each one, and makes them interchangeable. This pattern lets the algorithm
vary independently from clients that use it. The Strategy pattern is useful when you
have multiple ways to perform a task and want to be able to switch between them at runtime.
This is a longer text to demonstrate the effectiveness of different compression algorithms.
""" * 10 # Repeat to make it longer for better compression demonstration
# Create compressor
compressor = DataCompressor()
print("Data Compression Strategy Pattern Example")
print("=" * 50)
# Test individual strategies
strategies = [NoCompression(), GZIPCompression(), ZLIBCompression()]
for strategy in strategies:
compressor.set_compression_strategy(strategy)
compressed_data, stats = compressor.compress_data(sample_data)
print(f"\nUsing {strategy.get_algorithm_name()} compression:")
print(f"Original size: {stats['original_size']} bytes")
print(f"Compressed size: {stats['compressed_size']} bytes")
print(f"Compression ratio: {stats['compression_ratio']:.2f}%")
# Verify decompression works
decompressed_data = compressor.decompress_data(compressed_data)
print(f"Decompression successful: {decompressed_data == sample_data}")
print("\n" + "=" * 50)
# Compare all strategies
compressor.compare_strategies(sample_data, strategies)
Implementation Considerations
When implementing the Strategy pattern, consider these important aspects:
Strategy Selection: Determine how strategies will be selected - through configuration, runtime conditions, or user input.
Performance: Different strategies may have varying performance characteristics. Consider the trade-offs between speed, memory usage, and output quality.
State Management: Decide whether strategies should maintain state or be stateless. Stateless strategies are generally preferred for thread safety.
Error Handling: Ensure consistent error handling across all strategy implementations.
Strategy Registration: For systems with many strategies, consider implementing a registry pattern to manage strategy discovery and selection.
The Strategy pattern promotes clean, maintainable code by separating algorithm implementation from the code that uses it, making systems more flexible and extensible.