When implementing multiple behaviors like discount strategies, the common trap is hardcoding logic into service classes or bloating factories with switch
statements. Let’s explore a cleaner, extensible way using:
✅ Strategy Pattern
✅ Factory Pattern
✅ Dependency Injection via Dictionary<Enum, Func<T>>
This approach is minimal, testable, and elegant—especially if you want to add more strategies later with zero friction.
🧠 The Scenario
Suppose you're building an e-commerce platform that supports various discount types:
- Percentage-based discount (e.g., 20% off)
- Fixed amount discount (e.g., $10 off)
- Buy One Get One Free (BOGO)
Each of these follows different logic, but they all return the final price.
🧱 Step 1: Define the Strategy Interface
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal unitPrice, int quantity);
}
🛠 Step 2: Implement Each Strategy
PercentageDiscount:
public class PercentageDiscount : IDiscountStrategy
{
private readonly decimal _percentage = 0.2m;
public decimal ApplyDiscount(decimal unitPrice, int quantity)
{
var total = unitPrice * quantity;
return total - (total * _percentage);
}
}
FixedAmountDiscount:
public class FixedAmountDiscount : IDiscountStrategy
{
private readonly decimal _amount = 10m;
public decimal ApplyDiscount(decimal unitPrice, int quantity)
{
var total = unitPrice * quantity;
return total - _amount;
}
}
BuyOneGetOneDiscount:
public class BuyOneGetOneDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal unitPrice, int quantity)
{
var payableQuantity = (quantity / 2) + (quantity % 2);
return unitPrice * payableQuantity;
}
}
🏭 Step 4: Implement the Factory
Instead of injecting IServiceProvider
, we inject a dictionary of factory functions. This keeps your factory clean and testable.
IDiscountFactory:
public interface IDiscountFactory
{
IDiscountStrategy GetStrategy(DiscountType type);
}
DiscountFactory (with DI Dictionary)
public class DiscountFactory : IDiscountFactory
{
private readonly IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>> _strategyResolvers;
public DiscountFactory(IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>> strategyResolvers)
{
_strategyResolvers = strategyResolvers;
}
public IDiscountStrategy GetStrategy(DiscountType type)
{
if (_strategyResolvers.TryGetValue(type, out var resolver))
{
return resolver();
}
throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported discount type: {type}");
}
}
💼 Step 5: Define the Purchase Service Interface
public interface IPurchaseService
{
decimal CalculateFinalPrice(decimal unitPrice, int quantity, DiscountType discountType);
}
💳 Step 6: Implement PurchaseService Using the Factory
public class PurchaseService : IPurchaseService
{
private readonly IDiscountFactory _discountFactory;
public PurchaseService(IDiscountFactory discountFactory)
{
_discountFactory = discountFactory;
}
public decimal CalculateFinalPrice(decimal unitPrice, int quantity, DiscountType discountType)
{
var strategy = _discountFactory.GetStrategy(discountType);
return strategy.ApplyDiscount(unitPrice, quantity);
}
}
🧩 Step 7: Register Everything in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<PercentageDiscount>();
builder.Services.AddScoped<FixedAmountDiscount>();
builder.Services.AddScoped<BuyOneGetOneDiscount>();
builder.Services.AddScoped<IReadOnlyDictionary<DiscountType, Func<IDiscountStrategy>>>(sp =>
new Dictionary<DiscountType, Func<IDiscountStrategy>>
{
[DiscountType.Percentage] = () => sp.GetRequiredService<PercentageDiscount>(),
[DiscountType.FixedAmount] = () => sp.GetRequiredService<FixedAmountDiscount>(),
[DiscountType.BuyOneGetOne] = () => sp.GetRequiredService<BuyOneGetOneDiscount>()
});
builder.Services.AddScoped<IDiscountFactory, DiscountFactory>();
builder.Services.AddScoped<IPurchaseService, PurchaseService>();
var app = builder.Build();
🧪 Step 8: Real-World Usage (e.g., Controller or Console Output)
Let’s demonstrate this in Program.cs
using console output (ideal for early testing or debugging):
using var scope = app.Services.CreateScope();
var purchaseService = scope.ServiceProvider.GetRequiredService<IPurchaseService>();
Console.WriteLine("=== DISCOUNT CALCULATOR ===");
var price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.BuyOneGetOne);
Console.WriteLine($"Buy 3 items at $100 each with BOGO => Final price: {price:C}");
price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.Percentage);
Console.WriteLine($"Buy 3 items at $100 each with 20% discount => Final price: {price:C}");
price = purchaseService.CalculateFinalPrice(100m, 3, DiscountType.FixedAmount);
Console.WriteLine($"Buy 3 items at $100 each with $10 off => Final price: {price:C}");
app.Run();
✅ Final Thoughts
This architecture blends clean code, testability, and maintainability. You gain the flexibility to plug in new discount logic without touching the core business service or polluting the factory with conditionals.
You can extend this pattern to other domains like:
- Shipping methods
- Tax strategies
- Report generators
- Notification channels