Strategy Pattern

The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm or behavior at runtime by encapsulating a family of interchangeable algorithms into separate classes. It allows a client to choose the appropriate algorithm dynamically, promoting flexibility and adherence to the Open/Closed Principle. This pattern is particularly useful when you need to switch between different implementations of a task or behavior without altering the context that uses them.

Key Components

  1. Strategy: An interface or abstract class defining a method for the algorithm or behavior.
  2. Concrete Strategy: Classes implementing the Strategy interface, each providing a specific implementation of the algorithm.
  3. Context: The class that uses a Strategy, maintaining a reference to a Strategy object and delegating the algorithm execution to it.
  4. Client: Configures the Context with a specific Strategy and triggers the behavior.

How It Works

  • The Strategy interface declares a method that all concrete strategies implement.
  • Each Concrete Strategy provides a different implementation of the method, encapsulating a specific algorithm or behavior.
  • The Context holds a reference to a Strategy and delegates calls to it, allowing the behavior to vary based on the chosen strategy.
  • The Client sets the Strategy in the Context and invokes the Context’s methods, which use the Strategy’s implementation.
  • Strategies can be swapped at runtime, enabling dynamic behavior changes.

Sample Implementation

A model a payment processing system where payments can be processed using different strategies: CreditCard, PayPal, or Crypto.
// Strategy Interface
interface PaymentStrategy {
    void pay(double amount);
}

// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card ending in " + cardNumber.substring(cardNumber.length() - 4));
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal account " + email);
    }
}

class CryptoPayment implements PaymentStrategy {
    private String walletAddress;

    public CryptoPayment(String walletAddress) {
        this.walletAddress = walletAddress;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Cryptocurrency wallet " + walletAddress);
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double amount) {
        if (paymentStrategy == null) {
            System.out.println("No payment strategy selected.");
            return;
        }
        paymentStrategy.pay(amount);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Create context
        ShoppingCart cart = new ShoppingCart();

        // Create strategies
        PaymentStrategy creditCard = new CreditCardPayment("1234-5678-9012-3456");
        PaymentStrategy payPal = new PayPalPayment("user@example.com");
        PaymentStrategy crypto = new CryptoPayment("1A2b3C4d5E6f");

        // Use different strategies
        System.out.println("Checkout with Credit Card:");
        cart.setPaymentStrategy(creditCard);
        cart.checkout(100.50);

        System.out.println("\nCheckout with PayPal:");
        cart.setPaymentStrategy(payPal);
        cart.checkout(75.25);

        System.out.println("\nCheckout with Crypto:");
        cart.setPaymentStrategy(crypto);
        cart.checkout(200.00);

        System.out.println("\nCheckout without strategy:");
        cart.setPaymentStrategy(null);
        cart.checkout(50.00);
    }
}
/*
Checkout with Credit Card:
Paid $100.5 using Credit Card ending in 3456

Checkout with PayPal:
Paid $75.25 using PayPal account user@example.com

Checkout with Crypto:
Paid $200.0 using Cryptocurrency wallet 1A2b3C4d5E6f

Checkout without strategy:
No payment strategy selected.
*/Code language: PHP (php)

Use case and Implementation

//StrategyDemo.java
//Define the Strategy Interface
interface InterestStrategy {
    double calculateInterest(double balance);
}
//Implement Concrete Strategies
class SavingsAccountInterest implements InterestStrategy {
    @Override
    public double calculateInterest(double balance) {
        return balance * 0.04; // 4% interest
    }
}
class FixedDepositInterest implements InterestStrategy {
    @Override
    public double calculateInterest(double balance) {
        return balance * 0.06; // 6% interest
    }
}
class CurrentAccountInterest implements InterestStrategy {
    @Override
    public double calculateInterest(double balance) {
        return balance * 0.02; // 2% interest
    }
}
//Create the Context
class Account {
    private InterestStrategy interestStrategy;
    private double balance;

    public Account(InterestStrategy interestStrategy, double balance) {
        this.interestStrategy = interestStrategy;
        this.balance = balance;
    }

    public void setInterestStrategy(InterestStrategy interestStrategy) {
        this.interestStrategy = interestStrategy;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
        } else {
            System.out.println("Insufficient balance");
        }
    }

    public double calculateInterest() {
        return interestStrategy.calculateInterest(balance);
    }
}
// Use the Strategy Pattern
public class StrategyDemo{
    public static void main(String[] args) {
        Account savingsAccount = new Account(new SavingsAccountInterest(), 1000.0);
        Account fixedDepositAccount = new Account(new FixedDepositInterest(), 1000.0);
        Account currentAccount = new Account(new CurrentAccountInterest(), 1000.0);

        System.out.println("Savings Account Interest: " + savingsAccount.calculateInterest());
        System.out.println("Fixed Deposit Account Interest: " + fixedDepositAccount.calculateInterest());
        System.out.println("Current Account Interest: " + currentAccount.calculateInterest());

        // Changing the interest strategy dynamically
        savingsAccount.setInterestStrategy(new FixedDepositInterest());
        System.out.println("Savings Account Interest after strategy change: " + savingsAccount.calculateInterest());
    }
}

/*
C:\>javac StrategyDemo.java
C:\>java StrategyDemo
Savings Account Interest: 40.0
Fixed Deposit Account Interest: 60.0
Current Account Interest: 20.0
Savings Account Interest after strategy change: 60.0
*/

Pros

  • Flexibility: Allows runtime selection of algorithms, making the system adaptable.
  • Open/Closed Principle: New strategies can be added without modifying the Context.
  • Single Responsibility: Each strategy encapsulates one algorithm, improving maintainability.
  • Loose Coupling: The Context depends on the Strategy interface, not concrete implementations.
  • Eliminates Conditionals: Replaces if-else or switch statements with polymorphic strategy objects.

Cons

  • Class Proliferation: Each new algorithm requires a new strategy class, increasing the number of classes.
  • Client Complexity: The client must know about and configure strategies, which can add complexity.
  • Overhead: For simple scenarios, the pattern may introduce unnecessary abstraction.
  • State Management: Strategies are typically stateless; managing state across strategies can be challenging.

When to Use

  • When you need to switch between different algorithms or behaviors at runtime.
  • When you want to avoid complex conditional logic for selecting behavior.
  • When you need to make algorithms interchangeable and extensible.
  • When you want to isolate algorithm-specific code from the rest of the system.

Real-World Example

  • Payment Systems: As shown, different payment methods (credit card, PayPal, crypto) are implemented as strategies.
  • Sorting Algorithms: A sorting application can switch between quicksort, mergesort, or bubblesort based on data size or type.
  • Navigation Systems: A GPS app selects routing strategies (fastest, shortest, scenic) based on user preference.
  • Compression Libraries: A file compressor uses different algorithms (ZIP, RAR, GZIP) depending on the file type.

The Strategy Pattern is a valuable tool in a software developer’s arsenal. It provides a structured way to define and manage multiple algorithms or behaviors, promoting flexibility, maintainability, and extensibility. However, it is essential to balance its use against the potential complexity it introduces. When applied judiciously, the Strategy Pattern can significantly enhance the robustness and adaptability of your application, making it easier to manage and extend over time.

 

Scroll to Top