# Strategy Pattern: Interchangeable Algorithms

# 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

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

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

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

1. **Use when algorithms vary** - Multiple implementations of same behavior
2. **Encapsulate completely** - Hide algorithm details from clients
3. **Inject strategies** - Use dependency injection for flexibility
4. **Document complexity** - Include time/space complexity information
5. **Provide factory methods** - Simplify strategy creation
6. **Use composition** - Combine strategies for complex behaviors

### ❌ Don'ts

1. **Don't overuse** - Simple if-else is fine for 2-3 options
2. **Don't expose internals** - Keep algorithm details private
3. **Don't share state** - Make strategies stateless when possible
4. **Don't hardcode selection** - Use configuration or parameters
5. **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

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