🧩

Strategy Pattern

Design Patterns Intermediate 1 min read 200 words

Define a family of algorithms, encapsulate each one, and make them interchangeable

Strategy Pattern

Intent

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Problem

You need different variations of an algorithm, and you want to switch between them at runtime without complex conditional statements.

Solution

// Strategy interface
public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal price, int quantity);
    string Name { get; }
}

// Concrete strategies
public class NoDiscount : IDiscountStrategy
{
    public string Name => "No Discount";

    public decimal CalculateDiscount(decimal price, int quantity) => 0;
}

public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;

    public PercentageDiscount(decimal percentage)
    {
        _percentage = percentage;
    }

    public string Name => $"{_percentage}% Off";

    public decimal CalculateDiscount(decimal price, int quantity)
    {
        return price * quantity * (_percentage / 100);
    }
}

public class BulkDiscount : IDiscountStrategy
{
    public string Name => "Bulk Discount";

    public decimal CalculateDiscount(decimal price, int quantity)
    {
        var discount = quantity switch
        {
            >= 100 => 0.25m,  // 25% off for 100+
            >= 50 => 0.20m,   // 20% off for 50-99
            >= 20 => 0.15m,   // 15% off for 20-49
            >= 10 => 0.10m,   // 10% off for 10-19
            _ => 0m
        };

        return price * quantity * discount;
    }
}

public class MembershipDiscount : IDiscountStrategy
{
    private readonly MembershipLevel _level;

    public MembershipDiscount(MembershipLevel level)
    {
        _level = level;
    }

    public string Name => $"{_level} Member Discount";

    public decimal CalculateDiscount(decimal price, int quantity)
    {
        var discount = _level switch
        {
            MembershipLevel.Platinum => 0.30m,
            MembershipLevel.Gold => 0.20m,
            MembershipLevel.Silver => 0.10m,
            MembershipLevel.Bronze => 0.05m,
            _ => 0m
        };

        return price * quantity * discount;
    }
}

public class SeasonalDiscount : IDiscountStrategy
{
    public string Name => "Seasonal Sale";

    public decimal CalculateDiscount(decimal price, int quantity)
    {
        var discount = DateTime.Now.Month switch
        {
            12 => 0.25m,      // Christmas
            11 => 0.20m,      // Black Friday month
            7 or 8 => 0.15m,  // Summer sale
            _ => 0m
        };

        return price * quantity * discount;
    }
}

// Context - uses the strategy
public class PricingService
{
    private IDiscountStrategy _strategy;

    public PricingService(IDiscountStrategy strategy = null)
    {
        _strategy = strategy ?? new NoDiscount();
    }

    public void SetStrategy(IDiscountStrategy strategy)
    {
        _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
    }

    public PriceBreakdown CalculatePrice(Product product, int quantity)
    {
        var subtotal = product.Price * quantity;
        var discount = _strategy.CalculateDiscount(product.Price, quantity);

        return new PriceBreakdown
        {
            Subtotal = subtotal,
            DiscountName = _strategy.Name,
            DiscountAmount = discount,
            Total = subtotal - discount
        };
    }
}

// Usage
var pricingService = new PricingService();
var product = new Product { Name = "Widget", Price = 100m };

// No discount
var result = pricingService.CalculatePrice(product, 5);
Console.WriteLine($"Total: ${result.Total}"); // $500

// Apply bulk discount
pricingService.SetStrategy(new BulkDiscount());
result = pricingService.CalculatePrice(product, 50);
Console.WriteLine($"Total: ${result.Total}"); // $4000 (20% off)

// Apply membership discount
pricingService.SetStrategy(new MembershipDiscount(MembershipLevel.Gold));
result = pricingService.CalculatePrice(product, 5);
Console.WriteLine($"Total: ${result.Total}"); // $400 (20% off)

Strategy with Dependency Injection

// Register multiple strategies
services.AddTransient<NoDiscount>();
services.AddTransient<BulkDiscount>();
services.AddTransient<SeasonalDiscount>();

// Strategy selector
public interface IDiscountStrategySelector
{
    IDiscountStrategy SelectStrategy(Customer customer, Order order);
}

public class DiscountStrategySelector : IDiscountStrategySelector
{
    private readonly IServiceProvider _services;

    public DiscountStrategySelector(IServiceProvider services)
    {
        _services = services;
    }

    public IDiscountStrategy SelectStrategy(Customer customer, Order order)
    {
        // Business logic to select best strategy
        if (customer.MembershipLevel != MembershipLevel.None)
        {
            return new MembershipDiscount(customer.MembershipLevel);
        }

        if (order.TotalQuantity >= 10)
        {
            return _services.GetRequiredService<BulkDiscount>();
        }

        if (IsSeasonalSale())
        {
            return _services.GetRequiredService<SeasonalDiscount>();
        }

        return _services.GetRequiredService<NoDiscount>();
    }

    private bool IsSeasonalSale() =>
        DateTime.Now.Month is 7 or 8 or 11 or 12;
}

Strategy with Func Delegates

For simple strategies, use delegates instead of classes:

public class SortingService<T>
{
    public List<T> Sort(List<T> items, Func<T, T, int> comparer)
    {
        var result = new List<T>(items);
        result.Sort((a, b) => comparer(a, b));
        return result;
    }
}

// Usage with lambda strategies
var service = new SortingService<Product>();
var products = GetProducts();

// Sort by price ascending
var byPrice = service.Sort(products, (a, b) => a.Price.CompareTo(b.Price));

// Sort by name
var byName = service.Sort(products, (a, b) => a.Name.CompareTo(b.Name));

// Sort by popularity descending
var byPopularity = service.Sort(products, (a, b) => b.Sales.CompareTo(a.Sales));

Real-World Example: Payment Processing

public interface IPaymentStrategy
{
    Task<PaymentResult> ProcessAsync(PaymentRequest request);
    bool Supports(string method);
}

public class CreditCardStrategy : IPaymentStrategy
{
    private readonly IPaymentGateway _gateway;

    public CreditCardStrategy(IPaymentGateway gateway)
    {
        _gateway = gateway;
    }

    public bool Supports(string method) =>
        method.Equals("creditcard", StringComparison.OrdinalIgnoreCase);

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Validate card
        if (!ValidateCard(request.CardNumber))
            return PaymentResult.Failed("Invalid card number");

        // Process through gateway
        return await _gateway.ChargeCardAsync(request);
    }

    private bool ValidateCard(string cardNumber) =>
        cardNumber?.Length == 16 && cardNumber.All(char.IsDigit);
}

public class PayPalStrategy : IPaymentStrategy
{
    private readonly IPayPalClient _client;

    public PayPalStrategy(IPayPalClient client)
    {
        _client = client;
    }

    public bool Supports(string method) =>
        method.Equals("paypal", StringComparison.OrdinalIgnoreCase);

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        var redirectUrl = await _client.CreatePaymentAsync(request.Amount);
        return PaymentResult.Redirect(redirectUrl);
    }
}

public class BankTransferStrategy : IPaymentStrategy
{
    public bool Supports(string method) =>
        method.Equals("banktransfer", StringComparison.OrdinalIgnoreCase);

    public Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Generate bank transfer instructions
        return Task.FromResult(PaymentResult.Pending(
            "Please transfer to account XXXX-XXXX"));
    }
}

// Payment service uses strategies
public class PaymentService
{
    private readonly IEnumerable<IPaymentStrategy> _strategies;

    public PaymentService(IEnumerable<IPaymentStrategy> strategies)
    {
        _strategies = strategies;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        string method,
        PaymentRequest request)
    {
        var strategy = _strategies.FirstOrDefault(s => s.Supports(method));

        if (strategy == null)
            return PaymentResult.Failed($"Payment method '{method}' not supported");

        return await strategy.ProcessAsync(request);
    }
}

// Registration
services.AddTransient<IPaymentStrategy, CreditCardStrategy>();
services.AddTransient<IPaymentStrategy, PayPalStrategy>();
services.AddTransient<IPaymentStrategy, BankTransferStrategy>();
services.AddTransient<PaymentService>();

Combining Multiple Strategies

public class CompositeDiscountStrategy : IDiscountStrategy
{
    private readonly List<IDiscountStrategy> _strategies;
    private readonly CombineMode _mode;

    public string Name => $"Combined ({_mode})";

    public enum CombineMode
    {
        TakeBest,    // Use highest discount
        TakeFirst,   // Use first applicable
        StackAll     // Sum all discounts
    }

    public CompositeDiscountStrategy(
        IEnumerable<IDiscountStrategy> strategies,
        CombineMode mode = CombineMode.TakeBest)
    {
        _strategies = strategies.ToList();
        _mode = mode;
    }

    public decimal CalculateDiscount(decimal price, int quantity)
    {
        var discounts = _strategies
            .Select(s => s.CalculateDiscount(price, quantity))
            .Where(d => d > 0)
            .ToList();

        return _mode switch
        {
            CombineMode.TakeBest => discounts.DefaultIfEmpty(0).Max(),
            CombineMode.TakeFirst => discounts.FirstOrDefault(),
            CombineMode.StackAll => discounts.Sum(),
            _ => 0
        };
    }
}

Strategy vs If-Else

// WITHOUT Strategy - hard to maintain
public decimal CalculateShipping(string method, decimal weight)
{
    if (method == "standard")
        return weight * 0.5m;
    else if (method == "express")
        return weight * 1.5m;
    else if (method == "overnight")
        return weight * 3.0m;
    else if (method == "international")
        return weight * 5.0m + 10m;
    // Adding new method requires changing this code
    else
        throw new ArgumentException("Unknown method");
}

// WITH Strategy - extensible
public interface IShippingStrategy
{
    decimal Calculate(decimal weight);
}

public class StandardShipping : IShippingStrategy
{
    public decimal Calculate(decimal weight) => weight * 0.5m;
}

// Add new shipping methods without changing existing code
public class DroneShipping : IShippingStrategy
{
    public decimal Calculate(decimal weight) => weight * 2.0m + 5m;
}

Interview Tips

Common Questions:

  • “Explain Strategy pattern with an example”
  • “How is Strategy different from State pattern?”
  • “When would you use Strategy vs polymorphism?”

Key Points:

  1. Strategy encapsulates interchangeable algorithms
  2. Client code decides which strategy to use
  3. Strategies are switched at runtime
  4. Great for replacing complex conditionals
  5. Works well with dependency injection

Strategy vs State:

  • Strategy: Client chooses algorithm, doesn’t change automatically
  • State: Object changes behavior based on internal state automatically

.NET Examples:

  • IComparer<T> - sorting strategies
  • Func<T> delegates - function strategies
  • IEqualityComparer<T> - comparison strategies

📚 Related Articles