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?
- Problem: Without Strategy Pattern
- Solution: Strategy Pattern Implementation
- Advanced Example: Sorting Algorithms
- Strategy Pattern with Dependency Injection
- Advanced Patterns and Best Practices
- Testing Strategies
- Performance Considerations
- When to Use the Strategy Pattern
- Common Pitfalls and How to Avoid Them
- Conclusion
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
- Strategy Interface: Defines a common interface for all concrete strategies
- Concrete Strategies: Implement different algorithms using the Strategy interface
- Context: Maintains a reference to a strategy object and delegates algorithm execution
- Client: Creates and configures the context with the appropriate strategy
Key Benefits
- Open/Closed Principle: Easy to add new algorithms without modifying existing code
- Eliminates Conditional Logic: Replaces complex if/else or switch statements
- Runtime Flexibility: Algorithms can be changed at runtime
- Testability: Each strategy can be tested independently
- Single Responsibility: Each strategy focuses on one algorithm
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:
- Violates Open/Closed Principle (must modify class to add new customer types)
- Complex conditional logic is hard to maintain
- Difficult to test individual discount strategies
- Poor readability as the number of conditions grows
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:
- You have multiple ways to perform a task and want to switch between them at runtime.
- You want to eliminate complex conditional logic (if/else or switch statements).
- You need to make algorithms interchangeable and testable.
- You want to follow the Open/Closed Principle for adding new algorithms.
- You have a family of related algorithms that should be encapsulated.
Don’t use the Strategy pattern when:
- You only have one algorithm and it’s unlikely to change.
- The algorithms are very simple and don’t warrant separate classes.
- The overhead of creating strategy objects is greater than the benefit.
- The strategies have significantly different interfaces or dependencies.
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:
- Eliminate complex conditional logic.
- Follow the Open/Closed Principle.
- Make your code more testable and maintainable.
- Allow runtime algorithm switching.
- Promote code reusability.
Key takeaways:
- Define clear strategy interfaces that all concrete strategies implement.
- Keep strategies stateless to avoid unexpected behavior.
- Use factories or registries to centralize strategy selection logic.
- Integrate with dependency injection for better testability and flexibility.
- Consider performance implications when creating many strategy instances.
- Test strategies independently for better code coverage and reliability.
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.