Skip to content
Go back

Strategy Design Pattern

Published:

The Strategy design pattern is a powerful behavioral pattern that enables you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. This pattern is particularly useful when you have multiple ways to perform a task and want to switch between them dynamically.

In this comprehensive guide, we’ll explore the Strategy pattern in C# with practical examples, implementation strategies, and best practices that will help you write more flexible and maintainable code.

Table of Contents

Open Table of Contents

What is the Strategy Design Pattern?

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm can vary independently from clients that use it. This pattern is also known as the Policy pattern.

Instead of implementing multiple algorithms directly within a class using conditional statements, the Strategy pattern delegates the algorithm implementation to separate strategy classes.

Key Components

Key Benefits

Problem: Without Strategy Pattern

Let’s look at a common problem that the Strategy pattern solves. Consider a discount calculation system for an e-commerce application:

public class BadDiscountCalculator
{
    public decimal CalculateDiscount(decimal amount, string customerType, bool hasLoyaltyCard)
    {
        if (customerType == "Regular")
        {
            return hasLoyaltyCard ? amount * 0.05m : 0;
        }
        else if (customerType == "Premium")
        {
            return hasLoyaltyCard ? amount * 0.15m : amount * 0.10m;
        }
        else if (customerType == "VIP")
        {
            return hasLoyaltyCard ? amount * 0.25m : amount * 0.20m;
        }
        else if (customerType == "Employee")
        {
            return amount * 0.30m;
        }
        
        return 0;
    }
}

Problems with this approach:

Solution: Strategy Pattern Implementation

Here’s how we can refactor the discount calculation using the Strategy pattern:

1. Define the Strategy Interface

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard);
    string GetDescription();
}

2. Implement Concrete Strategies

public class RegularCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        return hasLoyaltyCard ? amount * 0.05m : 0;
    }

    public string GetDescription()
    {
        return "Regular customer discount";
    }
}

public class PremiumCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        return hasLoyaltyCard ? amount * 0.15m : amount * 0.10m;
    }

    public string GetDescription()
    {
        return "Premium customer discount";
    }
}

public class VipCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        return hasLoyaltyCard ? amount * 0.25m : amount * 0.20m;
    }

    public string GetDescription()
    {
        return "VIP customer discount";
    }
}

public class EmployeeDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        return amount * 0.30m; // Employees always get 30% discount
    }

    public string GetDescription()
    {
        return "Employee discount";
    }
}

3. Create the Context Class

public class ShoppingCart
{
    private IDiscountStrategy _discountStrategy;
    private readonly List<CartItem> _items;

    public ShoppingCart(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy ?? throw new ArgumentNullException(nameof(discountStrategy));
        _items = new List<CartItem>();
    }

    public void SetDiscountStrategy(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy ?? throw new ArgumentNullException(nameof(discountStrategy));
    }

    public void AddItem(CartItem item)
    {
        _items.Add(item);
    }

    public decimal GetSubtotal()
    {
        return _items.Sum(item => item.Price * item.Quantity);
    }

    public decimal CalculateDiscount(bool hasLoyaltyCard)
    {
        var subtotal = GetSubtotal();
        return _discountStrategy.CalculateDiscount(subtotal, hasLoyaltyCard);
    }

    public decimal GetTotal(bool hasLoyaltyCard)
    {
        var subtotal = GetSubtotal();
        var discount = CalculateDiscount(hasLoyaltyCard);
        return subtotal - discount;
    }

    public string GetDiscountDescription()
    {
        return _discountStrategy.GetDescription();
    }
}

public class CartItem
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

4. Usage Example

class Program
{
    static void Main(string[] args)
    {
        // Create a shopping cart for a premium customer
        var cart = new ShoppingCart(new PremiumCustomerDiscount());
        
        // Add items to cart
        cart.AddItem(new CartItem { Name = "Laptop", Price = 1000m, Quantity = 1 });
        cart.AddItem(new CartItem { Name = "Mouse", Price = 25m, Quantity = 2 });
        
        // Calculate totals
        var subtotal = cart.GetSubtotal();
        var discount = cart.CalculateDiscount(hasLoyaltyCard: true);
        var total = cart.GetTotal(hasLoyaltyCard: true);
        
        Console.WriteLine($"Subtotal: ${subtotal:F2}");
        Console.WriteLine($"Discount ({cart.GetDiscountDescription()}): ${discount:F2}");
        Console.WriteLine($"Total: ${total:F2}");
        
        // Change strategy at runtime
        Console.WriteLine("\nUpgrading to VIP customer...");
        cart.SetDiscountStrategy(new VipCustomerDiscount());
        
        var newDiscount = cart.CalculateDiscount(hasLoyaltyCard: true);
        var newTotal = cart.GetTotal(hasLoyaltyCard: true);
        
        Console.WriteLine($"New discount ({cart.GetDiscountDescription()}): ${newDiscount:F2}");
        Console.WriteLine($"New total: ${newTotal:F2}");
    }
}

Output:

Subtotal: $1050.00
Discount (Premium customer discount): $157.50
Total: $892.50

Upgrading to VIP customer...
New discount (VIP customer discount): $262.50
New total: $787.50

Advanced Example: Sorting Algorithms

Here’s another practical example demonstrating the Strategy pattern with different sorting algorithms:

1. Strategy Interface for Sorting

public interface ISortingStrategy<T> where T : IComparable<T>
{
    void Sort(T[] array);
    string GetAlgorithmName();
    TimeComplexity GetTimeComplexity();
}

public enum TimeComplexity
{
    Constant,
    Logarithmic,
    Linear,
    LinearLogarithmic,
    Quadratic,
    Exponential
}

2. Concrete Sorting Strategies

public class BubbleSortStrategy<T> : ISortingStrategy<T> where T : IComparable<T>
{
    public void Sort(T[] array)
    {
        int n = array.Length;
        for (int i = 0; i < n - 1; i++)
        {
            for (int j = 0; j < n - i - 1; j++)
            {
                if (array[j].CompareTo(array[j + 1]) > 0)
                {
                    // Swap elements
                    (array[j], array[j + 1]) = (array[j + 1], array[j]);
                }
            }
        }
    }

    public string GetAlgorithmName() => "Bubble Sort";
    public TimeComplexity GetTimeComplexity() => TimeComplexity.Quadratic;
}

public class QuickSortStrategy<T> : ISortingStrategy<T> where T : IComparable<T>
{
    public void Sort(T[] array)
    {
        QuickSort(array, 0, array.Length - 1);
    }

    private void QuickSort(T[] array, int low, int high)
    {
        if (low < high)
        {
            int pivotIndex = Partition(array, low, high);
            QuickSort(array, low, pivotIndex - 1);
            QuickSort(array, pivotIndex + 1, high);
        }
    }

    private int Partition(T[] array, int low, int high)
    {
        T pivot = array[high];
        int i = low - 1;

        for (int j = low; j < high; j++)
        {
            if (array[j].CompareTo(pivot) <= 0)
            {
                i++;
                (array[i], array[j]) = (array[j], array[i]);
            }
        }

        (array[i + 1], array[high]) = (array[high], array[i + 1]);
        return i + 1;
    }

    public string GetAlgorithmName() => "Quick Sort";
    public TimeComplexity GetTimeComplexity() => TimeComplexity.LinearLogarithmic;
}

public class MergeSortStrategy<T> : ISortingStrategy<T> where T : IComparable<T>
{
    public void Sort(T[] array)
    {
        MergeSort(array, 0, array.Length - 1);
    }

    private void MergeSort(T[] array, int left, int right)
    {
        if (left < right)
        {
            int middle = (left + right) / 2;
            MergeSort(array, left, middle);
            MergeSort(array, middle + 1, right);
            Merge(array, left, middle, right);
        }
    }

    private void Merge(T[] array, int left, int middle, int right)
    {
        int leftSize = middle - left + 1;
        int rightSize = right - middle;

        T[] leftArray = new T[leftSize];
        T[] rightArray = new T[rightSize];

        Array.Copy(array, left, leftArray, 0, leftSize);
        Array.Copy(array, middle + 1, rightArray, 0, rightSize);

        int i = 0, j = 0, k = left;

        while (i < leftSize && j < rightSize)
        {
            if (leftArray[i].CompareTo(rightArray[j]) <= 0)
            {
                array[k++] = leftArray[i++];
            }
            else
            {
                array[k++] = rightArray[j++];
            }
        }

        while (i < leftSize) array[k++] = leftArray[i++];
        while (j < rightSize) array[k++] = rightArray[j++];
    }

    public string GetAlgorithmName() => "Merge Sort";
    public TimeComplexity GetTimeComplexity() => TimeComplexity.LinearLogarithmic;
}

3. Sorting Context

public class ArraySorter<T> where T : IComparable<T>
{
    private ISortingStrategy<T> _sortingStrategy;

    public ArraySorter(ISortingStrategy<T> sortingStrategy)
    {
        _sortingStrategy = sortingStrategy ?? throw new ArgumentNullException(nameof(sortingStrategy));
    }

    public void SetSortingStrategy(ISortingStrategy<T> sortingStrategy)
    {
        _sortingStrategy = sortingStrategy ?? throw new ArgumentNullException(nameof(sortingStrategy));
    }

    public void SortArray(T[] array)
    {
        if (array == null) throw new ArgumentNullException(nameof(array));
        if (array.Length <= 1) return;

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        _sortingStrategy.Sort(array);
        stopwatch.Stop();

        Console.WriteLine($"Sorted using {_sortingStrategy.GetAlgorithmName()}");
        Console.WriteLine($"Time complexity: {_sortingStrategy.GetTimeComplexity()}");
        Console.WriteLine($"Execution time: {stopwatch.ElapsedMilliseconds} ms");
    }

    public ISortingStrategy<T> GetCurrentStrategy() => _sortingStrategy;
}

4. Sorting Example Usage

class Program
{
    static void Main(string[] args)
    {
        // Create test data
        var numbers = new int[] { 64, 34, 25, 12, 22, 11, 90, 88, 76, 50, 42 };
        
        Console.WriteLine($"Original array: [{string.Join(", ", numbers)}]");
        Console.WriteLine();

        // Test different sorting strategies
        var strategies = new ISortingStrategy<int>[]
        {
            new BubbleSortStrategy<int>(),
            new QuickSortStrategy<int>(),
            new MergeSortStrategy<int>()
        };

        foreach (var strategy in strategies)
        {
            var testArray = (int[])numbers.Clone(); // Create a copy for each test
            var sorter = new ArraySorter<int>(strategy);
            
            sorter.SortArray(testArray);
            Console.WriteLine($"Sorted array: [{string.Join(", ", testArray)}]");
            Console.WriteLine();
        }
    }
}

Strategy Pattern with Dependency Injection

Modern C# applications often use dependency injection. Here’s how to integrate the Strategy pattern with DI:

1. Strategy Factory

public interface IDiscountStrategyFactory
{
    IDiscountStrategy CreateStrategy(CustomerType customerType);
}

public class DiscountStrategyFactory : IDiscountStrategyFactory
{
    private readonly IServiceProvider _serviceProvider;

    public DiscountStrategyFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IDiscountStrategy CreateStrategy(CustomerType customerType)
    {
        return customerType switch
        {
            CustomerType.Regular => _serviceProvider.GetRequiredService<RegularCustomerDiscount>(),
            CustomerType.Premium => _serviceProvider.GetRequiredService<PremiumCustomerDiscount>(),
            CustomerType.VIP => _serviceProvider.GetRequiredService<VipCustomerDiscount>(),
            CustomerType.Employee => _serviceProvider.GetRequiredService<EmployeeDiscount>(),
            _ => throw new ArgumentException($"Unknown customer type: {customerType}")
        };
    }
}

public enum CustomerType
{
    Regular,
    Premium,
    VIP,
    Employee
}

2. DI Registration

// In Startup.cs or Program.cs
services.AddScoped<RegularCustomerDiscount>();
services.AddScoped<PremiumCustomerDiscount>();
services.AddScoped<VipCustomerDiscount>();
services.AddScoped<EmployeeDiscount>();
services.AddScoped<IDiscountStrategyFactory, DiscountStrategyFactory>();

3. Usage with DI

public class OrderService
{
    private readonly IDiscountStrategyFactory _strategyFactory;

    public OrderService(IDiscountStrategyFactory strategyFactory)
    {
        _strategyFactory = strategyFactory;
    }

    public decimal CalculateOrderTotal(Order order, CustomerType customerType, bool hasLoyaltyCard)
    {
        var strategy = _strategyFactory.CreateStrategy(customerType);
        var subtotal = order.Items.Sum(item => item.Price * item.Quantity);
        var discount = strategy.CalculateDiscount(subtotal, hasLoyaltyCard);
        
        return subtotal - discount;
    }
}

Advanced Patterns and Best Practices

1. Strategy with Configuration

public class ConfigurableDiscountStrategy : IDiscountStrategy
{
    private readonly DiscountConfiguration _config;

    public ConfigurableDiscountStrategy(DiscountConfiguration config)
    {
        _config = config;
    }

    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        var baseDiscount = amount * _config.BaseDiscountRate;
        var loyaltyBonus = hasLoyaltyCard ? amount * _config.LoyaltyBonusRate : 0;
        
        return Math.Min(baseDiscount + loyaltyBonus, amount * _config.MaxDiscountRate);
    }

    public string GetDescription() => _config.Description;
}

public class DiscountConfiguration
{
    public decimal BaseDiscountRate { get; set; }
    public decimal LoyaltyBonusRate { get; set; }
    public decimal MaxDiscountRate { get; set; }
    public string Description { get; set; }
}

2. Async Strategy Pattern

public interface IAsyncPaymentStrategy
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
    string GetProviderName();
}

public class StripePaymentStrategy : IAsyncPaymentStrategy
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<StripePaymentStrategy> _logger;

    public StripePaymentStrategy(HttpClient httpClient, ILogger<StripePaymentStrategy> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        try
        {
            _logger.LogInformation("Processing Stripe payment for amount: {Amount}", request.Amount);
            
            // Simulate API call to Stripe
            await Task.Delay(1000); // Simulate network latency
            
            return new PaymentResult
            {
                Success = true,
                TransactionId = Guid.NewGuid().ToString(),
                ProcessedAt = DateTime.UtcNow,
                Provider = GetProviderName()
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe payment processing failed");
            return new PaymentResult { Success = false, ErrorMessage = ex.Message };
        }
    }

    public string GetProviderName() => "Stripe";
}

public class PaymentProcessor
{
    private IAsyncPaymentStrategy _strategy;

    public PaymentProcessor(IAsyncPaymentStrategy strategy)
    {
        _strategy = strategy;
    }

    public void SetStrategy(IAsyncPaymentStrategy strategy)
    {
        _strategy = strategy;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        return await _strategy.ProcessPaymentAsync(request);
    }
}

Testing Strategies

1. Unit Testing Individual Strategies

[TestFixture]
public class DiscountStrategyTests
{
    [Test]
    public void PremiumCustomerDiscount_WithLoyaltyCard_Returns15Percent()
    {
        // Arrange
        var strategy = new PremiumCustomerDiscount();
        var amount = 100m;
        var hasLoyaltyCard = true;

        // Act
        var discount = strategy.CalculateDiscount(amount, hasLoyaltyCard);

        // Assert
        Assert.AreEqual(15m, discount);
    }

    [Test]
    public void PremiumCustomerDiscount_WithoutLoyaltyCard_Returns10Percent()
    {
        // Arrange
        var strategy = new PremiumCustomerDiscount();
        var amount = 100m;
        var hasLoyaltyCard = false;

        // Act
        var discount = strategy.CalculateDiscount(amount, hasLoyaltyCard);

        // Assert
        Assert.AreEqual(10m, discount);
    }
}

2. Testing Context with Mock Strategies

[TestFixture]
public class ShoppingCartTests
{
    [Test]
    public void CalculateDiscount_CallsStrategyWithCorrectParameters()
    {
        // Arrange
        var mockStrategy = new Mock<IDiscountStrategy>();
        mockStrategy.Setup(s => s.CalculateDiscount(100m, true)).Returns(15m);
        
        var cart = new ShoppingCart(mockStrategy.Object);
        cart.AddItem(new CartItem { Name = "Test", Price = 50m, Quantity = 2 });

        // Act
        var discount = cart.CalculateDiscount(hasLoyaltyCard: true);

        // Assert
        Assert.AreEqual(15m, discount);
        mockStrategy.Verify(s => s.CalculateDiscount(100m, true), Times.Once);
    }

    [Test]
    public void SetDiscountStrategy_ChangesStrategySuccessfully()
    {
        // Arrange
        var initialStrategy = new Mock<IDiscountStrategy>();
        var newStrategy = new Mock<IDiscountStrategy>();
        newStrategy.Setup(s => s.CalculateDiscount(It.IsAny<decimal>(), It.IsAny<bool>())).Returns(25m);
        
        var cart = new ShoppingCart(initialStrategy.Object);
        cart.AddItem(new CartItem { Name = "Test", Price = 100m, Quantity = 1 });

        // Act
        cart.SetDiscountStrategy(newStrategy.Object);
        var discount = cart.CalculateDiscount(hasLoyaltyCard: false);

        // Assert
        Assert.AreEqual(25m, discount);
        newStrategy.Verify(s => s.CalculateDiscount(100m, false), Times.Once);
    }
}

Performance Considerations

1. Strategy Caching

public class CachedStrategyFactory : IDiscountStrategyFactory
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ConcurrentDictionary<CustomerType, IDiscountStrategy> _strategyCache;

    public CachedStrategyFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _strategyCache = new ConcurrentDictionary<CustomerType, IDiscountStrategy>();
    }

    public IDiscountStrategy CreateStrategy(CustomerType customerType)
    {
        return _strategyCache.GetOrAdd(customerType, type =>
        {
            return type switch
            {
                CustomerType.Regular => _serviceProvider.GetRequiredService<RegularCustomerDiscount>(),
                CustomerType.Premium => _serviceProvider.GetRequiredService<PremiumCustomerDiscount>(),
                CustomerType.VIP => _serviceProvider.GetRequiredService<VipCustomerDiscount>(),
                CustomerType.Employee => _serviceProvider.GetRequiredService<EmployeeDiscount>(),
                _ => throw new ArgumentException($"Unknown customer type: {type}")
            };
        });
    }
}

2. Memory-Efficient Strategies

public static class DiscountStrategies
{
    public static readonly IDiscountStrategy Regular = new RegularCustomerDiscount();
    public static readonly IDiscountStrategy Premium = new PremiumCustomerDiscount();
    public static readonly IDiscountStrategy VIP = new VipCustomerDiscount();
    public static readonly IDiscountStrategy Employee = new EmployeeDiscount();

    public static IDiscountStrategy GetStrategy(CustomerType customerType)
    {
        return customerType switch
        {
            CustomerType.Regular => Regular,
            CustomerType.Premium => Premium,
            CustomerType.VIP => VIP,
            CustomerType.Employee => Employee,
            _ => throw new ArgumentException($"Unknown customer type: {customerType}")
        };
    }
}

When to Use the Strategy Pattern

Use the Strategy pattern when:

Don’t use the Strategy pattern when:

Common Pitfalls and How to Avoid Them

1. Strategy Selection Logic

Problem: Complex strategy selection logic scattered throughout the application.

Solution: Use a factory or registry pattern to centralize strategy selection.

public class StrategyRegistry<TKey, TStrategy>
{
    private readonly Dictionary<TKey, TStrategy> _strategies = new();

    public void Register(TKey key, TStrategy strategy)
    {
        _strategies[key] = strategy;
    }

    public TStrategy GetStrategy(TKey key)
    {
        if (_strategies.TryGetValue(key, out var strategy))
        {
            return strategy;
        }
        
        throw new ArgumentException($"No strategy registered for key: {key}");
    }

    public bool HasStrategy(TKey key) => _strategies.ContainsKey(key);
}

2. Strategy State Management

Problem: Strategies that maintain state between calls can cause unexpected behavior.

Solution: Keep strategies stateless or ensure proper state management.

// Bad: Stateful strategy
public class BadDiscountStrategy : IDiscountStrategy
{
    private decimal _totalDiscountsApplied; // State!

    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        var discount = amount * 0.1m;
        _totalDiscountsApplied += discount; // Dangerous!
        return discount;
    }
}

// Good: Stateless strategy
public class GoodDiscountStrategy : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount, bool hasLoyaltyCard)
    {
        return amount * 0.1m; // Pure function
    }
}

Conclusion

The Strategy design pattern is a powerful tool for creating flexible, maintainable, and testable code. By encapsulating algorithms in separate classes and making them interchangeable, you can:

Key takeaways:

The Strategy pattern shines in scenarios where you need flexibility in algorithm selection, such as payment processing, data validation, sorting algorithms, or business rule engines. By mastering this pattern, you’ll be able to write more robust and adaptable C# applications.

Remember: the goal is not to use patterns for their own sake, but to solve real problems. Use the Strategy pattern when you genuinely need the flexibility it provides, and always consider the simplest solution that meets your requirements.



Previous Post
Dunning-Kruger effect
Next Post
Factory Design Pattern in C#: Complete Guide with Examples