Strategy Pattern in C# for Interchangeable Algorithms
The Strategy design pattern is a behavioral pattern that allows you to define a family of algorithms, encapsulate each one inside a separate class, and make them interchangeable at runtime. This article provides a direct, practical guide to implementing the Strategy pattern in C#. You will learn how to decouple algorithm logic from business logic using interfaces, build concrete strategy classes, implement a context class, and dynamically switch behaviors using modern C# practices.
Core Components of the Strategy Pattern
The pattern consists of three primary components:
- Strategy Interface: Declares a common interface for all supported algorithms.
- Concrete Strategies: Classes that implement the strategy interface with specific algorithmic logic.
- Context: A class that maintains a reference to a strategy object and delegates the algorithmic task to it instead of implementing the behavior directly.
Step 1: Define the Strategy Interface
Create an interface that standardizes the algorithm's execution signature. For this example, consider a payment processing scenario where different payment methods require different processing logic.
public interface IPaymentStrategy
{
void Pay(decimal amount);
}Step 2: Implement Concrete Strategies
Create concrete classes that implement the interface. Each class encapsulates an isolated algorithm or mechanism.
public class CreditCardPayment : IPaymentStrategy
{
private readonly string _cardNumber;
public CreditCardPayment(string cardNumber)
{
_cardNumber = cardNumber;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid {amount:C} using Credit Card ending in {_cardNumber[^4..]}.");
}
}
public class PayPalPayment : IPaymentStrategy
{
private readonly string _email;
public PayPalPayment(string email)
{
_email = email;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid {amount:C} using PayPal account ({_email}).");
}
}
public class CryptoPayment : IPaymentStrategy
{
private readonly string _walletAddress;
public CryptoPayment(string walletAddress)
{
_walletAddress = walletAddress;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid {amount:C} using Crypto Wallet: {_walletAddress}.");
}
}Step 3: Implement the Context Class
The context class references the strategy interface. It can receive the strategy via constructor injection or allow runtime mutation through a setter method.
public class CheckoutContext
{
private IPaymentStrategy _paymentStrategy;
public CheckoutContext(IPaymentStrategy initialStrategy)
{
_paymentStrategy = initialStrategy;
}
// Allows swapping the algorithm dynamically at runtime
public void SetPaymentStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void ExecuteCheckout(decimal totalAmount)
{
if (_paymentStrategy == null)
{
throw new InvalidOperationException("Payment strategy is not set.");
}
_paymentStrategy.Pay(totalAmount);
}
}Step 4: Execute the Pattern
Instantiate the context and dynamically swap strategies based on runtime conditions or user choices.
class Program
{
static void Main()
{
decimal orderAmount = 149.99m;
// Initialize with Credit Card strategy
var checkout = new CheckoutContext(new CreditCardPayment("1234567812345678"));
checkout.ExecuteCheckout(orderAmount);
// Switch algorithm to PayPal at runtime
checkout.SetPaymentStrategy(new PayPalPayment("user@example.com"));
checkout.ExecuteCheckout(orderAmount);
// Switch algorithm to Crypto at runtime
checkout.SetPaymentStrategy(new CryptoPayment("0x123abc456def"));
checkout.ExecuteCheckout(orderAmount);
}
}Using Dependency Injection in .NET
In modern .NET applications, you can register multiple concrete strategies with the built-in Dependency Injection (DI) container and resolve them using a factory delegate or a key:
// Program.cs / Service Registration
builder.Services.AddTransient<CreditCardPayment>();
builder.Services.AddTransient<PayPalPayment>();
// Factory delegate for strategy resolution
builder.Services.AddTransient<Func<string, IPaymentStrategy>>(serviceProvider => key =>
{
return key switch
{
"CreditCard" => serviceProvider.GetRequiredService<CreditCardPayment>(),
"PayPal" => serviceProvider.GetRequiredService<PayPalPayment>(),
_ => throw new KeyNotFoundException("Strategy not found.")
};
});When to Use the Strategy Pattern
- Multiple algorithmic variants: When you need different variations of an algorithm (e.g., sorting, compression, data export).
- Eliminating conditional logic: When code uses large
switchorif-elseblocks to select behaviors. - Isolating implementation details: When business logic should remain agnostic of complex, proprietary, or frequently changing algorithmic code.