Design patterns are frequently used methods in software development. They allow for optimizing, clarifying, and, most importantly, making the computer code more robust.

Strategy Pattern

A design pattern provides a solution to a frequently encountered problem in object-oriented programming. In the 1990s, the "Gang Of Four" highlighted three categories in their work "Design Patterns: Elements of Reusable Object-Oriented Software":

  • Creational patterns
  • Behavioral patterns
  • Structural patterns

The Strategy design pattern, or Strategy Design Pattern, is part of the behavioral patterns family. These patterns define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Problem Statement

Imagine that you need to perform a specific task that can be accomplished in multiple ways. For example, you might need to calculate shipping costs, process payments, or sort data. Each approach has its own algorithm, but you want to be able to switch between them at runtime.

The first idea you might have to solve this problem is to implement a large conditional statement, as follows:

switch (paymentMethod)
{
    case "CreditCard":
        ProcessCreditCardPayment();
        break;
    case "PayPal":
        ProcessPayPalPayment();
        break;
    case "BankTransfer":
        ProcessBankTransferPayment();
        break;
}

This approach becomes problematic when you need to add new payment methods or modify existing ones, as it violates the Open/Closed principle.

Solution with the Strategy Design Pattern

The main idea of the Strategy pattern is to take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies.

The original class, called the context, must have a field for storing a reference to one of the strategies. The context delegates the work to a linked strategy object instead of executing it on its own.

All strategies must implement the same interface so that the context can work with all strategies uniformly.

Strategy Pattern Structure Diagram

Strategy Pattern Structure Diagram

Theoretical Case: Example of Implementation

We are going to create a context class that will use different strategies:

using System;
public class Context
{
    // Reference to the current strategy
    private IStrategy _strategy;

    public Context(IStrategy strategy)
    {
        this._strategy = strategy;
    }

    public void SetStrategy(IStrategy strategy)
    {
        Console.WriteLine($"Context: Strategy changed to {strategy.GetType().Name}");
        this._strategy = strategy;
    }

    public void ExecuteStrategy(string data)
    {
        var result = this._strategy.Execute(data);
        Console.WriteLine($"Result: {result}");
    }
}

We declare the strategy interface that will define the contract for all concrete strategies:

public interface IStrategy
{
    string Execute(string data);
}

For each algorithm, we create a class that implements this interface, containing the specific implementation:

public class ConcreteStrategyA : IStrategy
{
    public string Execute(string data)
    {
        Console.WriteLine("ConcreteStrategyA: Processing data using Algorithm A");
        return $"Processed by A: {data.ToUpper()}";
    }
}

public class ConcreteStrategyB : IStrategy
{
    public string Execute(string data)
    {
        Console.WriteLine("ConcreteStrategyB: Processing data using Algorithm B");
        return $"Processed by B: {string.Join("", data.Reverse())}";
    }
}

public class ConcreteStrategyC : IStrategy
{
    public string Execute(string data)
    {
        Console.WriteLine("ConcreteStrategyC: Processing data using Algorithm C");
        return $"Processed by C: {data.Replace(" ", "_")}";
    }
}

Finally, to use different strategies, we create instances and pass them to the context:

public class Program 
{
    public static void Main(string[] args)
    {
        var context = new Context(new ConcreteStrategyA());
        string testData = "Hello World";

        context.ExecuteStrategy(testData);
        
        context.SetStrategy(new ConcreteStrategyB());
        context.ExecuteStrategy(testData);
        
        context.SetStrategy(new ConcreteStrategyC());
        context.ExecuteStrategy(testData);
    }     
}

Concrete Case: The Payment Processing System

Let's assume that we have an e-commerce application that needs to process payments through different payment methods. Each payment method has its own processing logic, validation rules, and fee structure.

We want to support multiple payment methods: - Credit Card - PayPal - Bank Transfer

Payment Processing Strategy Diagram

Strategy Diagram

Let's implement our payment processing system. We begin by creating our context (here named PaymentProcessor):

using System;
public class PaymentProcessor
{
    private IPaymentStrategy _paymentStrategy;

    public PaymentProcessor(IPaymentStrategy paymentStrategy)
    {
        this._paymentStrategy = paymentStrategy;
    }

    public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
    {
        this._paymentStrategy = paymentStrategy;
    }

    public void ProcessPayment(decimal amount, string customerInfo)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Invalid payment amount");
            return;
        }

        Console.WriteLine($"Processing payment of ${amount:F2}");
        this._paymentStrategy.ProcessPayment(amount, customerInfo);
    }
}

Then our payment strategy interface along with our concrete implementations for each payment method:

public interface IPaymentStrategy
{
    void ProcessPayment(decimal amount, string customerInfo);
}

public class CreditCardPaymentStrategy : IPaymentStrategy
{
    private string _cardNumber;
    private string _expiryDate;
    private string _cvv;

    public CreditCardPaymentStrategy(string cardNumber, string expiryDate, string cvv)
    {
        _cardNumber = cardNumber;
        _expiryDate = expiryDate;
        _cvv = cvv;
    }

    public void ProcessPayment(decimal amount, string customerInfo)
    {
        // Apply credit card processing fee (2.9%)
        decimal fee = amount * 0.029m;
        decimal totalAmount = amount + fee;

        Console.WriteLine("=== Credit Card Payment ===");
        Console.WriteLine($"Card Number: ****-****-****-{_cardNumber.Substring(_cardNumber.Length - 4)}");
        Console.WriteLine($"Amount: ${amount:F2}");
        Console.WriteLine($"Processing Fee: ${fee:F2}");
        Console.WriteLine($"Total Charged: ${totalAmount:F2}");
        Console.WriteLine("Payment processed successfully via Credit Card");
    }
}

public class PayPalPaymentStrategy : IPaymentStrategy
{
    private string _email;

    public PayPalPaymentStrategy(string email)
    {
        _email = email;
    }

    public void ProcessPayment(decimal amount, string customerInfo)
    {
        // Apply PayPal processing fee (3.4%)
        decimal fee = amount * 0.034m;
        decimal totalAmount = amount + fee;

        Console.WriteLine("=== PayPal Payment ===");
        Console.WriteLine($"PayPal Account: {_email}");
        Console.WriteLine($"Amount: ${amount:F2}");
        Console.WriteLine($"Processing Fee: ${fee:F2}");
        Console.WriteLine($"Total Charged: ${totalAmount:F2}");
        Console.WriteLine("Payment processed successfully via PayPal");
    }
}

public class BankTransferPaymentStrategy : IPaymentStrategy
{
    private string _accountNumber;
    private string _routingNumber;

    public BankTransferPaymentStrategy(string accountNumber, string routingNumber)
    {
        _accountNumber = accountNumber;
        _routingNumber = routingNumber;
    }

    public void ProcessPayment(decimal amount, string customerInfo)
    {
        // Bank transfer typically has a flat fee
        decimal fee = 5.00m;
        decimal totalAmount = amount + fee;

        Console.WriteLine("=== Bank Transfer Payment ===");
        Console.WriteLine($"Account Number: ****{_accountNumber.Substring(_accountNumber.Length - 4)}");
        Console.WriteLine($"Routing Number: {_routingNumber}");
        Console.WriteLine($"Amount: ${amount:F2}");
        Console.WriteLine($"Processing Fee: ${fee:F2}");
        Console.WriteLine($"Total Charged: ${totalAmount:F2}");
        Console.WriteLine("Payment processed successfully via Bank Transfer");
        Console.WriteLine("Note: Bank transfer may take 2-3 business days to complete");
    }
}

Payment Processing Class Structure

Payment Processing Structure Diagram

To demonstrate our implementation, let's create a sample program that processes payments using different strategies:

public class Program
{
    public static void Main(string[] args)
    {
        decimal purchaseAmount = 100.00m;
        string customerInfo = "John Doe";

        // Process payment with Credit Card
        var creditCardStrategy = new CreditCardPaymentStrategy("1234567890123456", "12/25", "123");
        var paymentProcessor = new PaymentProcessor(creditCardStrategy);
        paymentProcessor.ProcessPayment(purchaseAmount, customerInfo);

        Console.WriteLine("\n" + new string('-', 50) + "\n");

        // Switch to PayPal
        var paypalStrategy = new PayPalPaymentStrategy("john.doe@email.com");
        paymentProcessor.SetPaymentStrategy(paypalStrategy);
        paymentProcessor.ProcessPayment(purchaseAmount, customerInfo);

        Console.WriteLine("\n" + new string('-', 50) + "\n");

        // Switch to Bank Transfer
        var bankTransferStrategy = new BankTransferPaymentStrategy("9876543210", "123456789");
        paymentProcessor.SetPaymentStrategy(bankTransferStrategy);
        paymentProcessor.ProcessPayment(purchaseAmount, customerInfo);
    }
}

This produces the following output:

Processing payment of $100.00
=== Credit Card Payment ===
Card Number: ****-****-****-3456
Amount: $100.00
Processing Fee: $2.90
Total Charged: $102.90
Payment processed successfully via Credit Card

--------------------------------------------------

Processing payment of $100.00
=== PayPal Payment ===
PayPal Account: john.doe@email.com
Amount: $100.00
Processing Fee: $3.40
Total Charged: $103.40
Payment processed successfully via PayPal

--------------------------------------------------

Processing payment of $100.00
=== Bank Transfer Payment ===
Account Number: ****3210
Routing Number: 123456789
Amount: $100.00
Processing Fee: $5.00
Total Charged: $105.00
Payment processed successfully via Bank Transfer
Note: Bank transfer may take 2-3 business days to complete

Advanced Example: Dynamic Strategy Selection

You can also implement dynamic strategy selection based on business rules:

public class SmartPaymentProcessor : PaymentProcessor
{
    public SmartPaymentProcessor() : base(new CreditCardPaymentStrategy("", "", ""))
    {
    }

    public void ProcessPaymentWithOptimalStrategy(decimal amount, string customerInfo, bool isPremiumCustomer)
    {
        IPaymentStrategy optimalStrategy;

        if (amount > 1000 && isPremiumCustomer)
        {
            // For large amounts and premium customers, use bank transfer (lowest fee for large amounts)
            optimalStrategy = new BankTransferPaymentStrategy("9876543210", "123456789");
            Console.WriteLine("Selected Bank Transfer for large premium transaction");
        }
        else if (amount < 50)
        {
            // For small amounts, use PayPal despite higher percentage (better user experience)
            optimalStrategy = new PayPalPaymentStrategy("customer@email.com");
            Console.WriteLine("Selected PayPal for small transaction");
        }
        else
        {
            // Default to credit card for medium amounts
            optimalStrategy = new CreditCardPaymentStrategy("1234567890123456", "12/25", "123");
            Console.WriteLine("Selected Credit Card for standard transaction");
        }

        SetPaymentStrategy(optimalStrategy);
        ProcessPayment(amount, customerInfo);
    }
}

Benefits of the Strategy Pattern

  1. Open/Closed Principle: You can introduce new strategies without changing existing code.
  2. Single Responsibility Principle: Each strategy handles one specific algorithm.
  3. Runtime Flexibility: You can switch algorithms at runtime.
  4. Elimination of Conditionals: Replaces large conditional statements with clean, maintainable classes.
  5. Testability: Each strategy can be tested independently.

When to Use the Strategy Pattern

  • When you have multiple ways to perform a task
  • When you want to switch algorithms at runtime
  • When you have a class with massive conditional statements
  • When you want to isolate the implementation details of algorithms
  • When many related classes differ only in their behavior

Conclusion

The Strategy pattern is an excellent solution when you need to define a family of algorithms and make them interchangeable. Unlike the State pattern which changes behavior based on internal state, the Strategy pattern allows you to change behavior by switching the algorithm being used. This pattern promotes loose coupling, enhances maintainability, and adheres to SOLID principles.

By encapsulating each algorithm in its own class, you create a flexible system that can easily accommodate new requirements without modifying existing code. Whether you're building payment processing systems, data validation logic, or any system that requires algorithmic flexibility, the Strategy pattern provides a robust foundation for your architecture.

The visual diagrams above illustrate how the Strategy pattern creates clear separation between the context and the algorithms, making it easy to understand, maintain, and extend your codebase.