Skip to main content

Command Palette

Search for a command to run...

Strategy Pattern: Interchangeable Algorithms

Learn: Strategy Pattern: Interchangeable Algorithms

Updated
6 min readView as Markdown
T

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

  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

# 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

AspectBenefit
FlexibilitySwitch algorithms at runtime
MaintainabilityEach algorithm isolated in own class
TestabilityMock strategies easily
ExtensibilityAdd new algorithms without modifying existing code
ClarityIntent explicit through strategy classes

The Strategy Pattern transforms rigid conditional logic into flexible, maintainable, and testable code by treating algorithms as first-class objects.