Strategy Pattern — the email's exact exercise
The DiscountService refactor, done properly, two ways (classic factory + .NET 8 keyed DI).
This is the one concrete task they sent you — be able to write this on a whiteboard/shared editor from memory, and be able to explain why each step helps, not just recite the code.
The problem code:
public class DiscountService
{
public double CalculateDiscount(string customerType, double price)
{
if (customerType == "Regular")
return price * 0.10;
if (customerType == "Premium")
return price * 0.20;
return 0;
}
}
Two smells: adding a new customer type means editing this method (Open/Closed violation), and
customerType is a raw string (Clean Code: primitive obsession — typos fail silently).
Step 1 — extract a common interface, one implementation per case:
public interface IDiscountStrategy
{
double Calculate(double price);
}
public class RegularDiscountStrategy : IDiscountStrategy
{
public double Calculate(double price) => price * 0.10;
}
public class PremiumDiscountStrategy : IDiscountStrategy
{
public double Calculate(double price) => price * 0.20;
}
public class NoDiscountStrategy : IDiscountStrategy
{
public double Calculate(double price) => 0;
}
Step 2a — classic factory + DI (works on any .NET version):
public interface IDiscountStrategyFactory
{
IDiscountStrategy GetStrategy(CustomerType customerType);
}
public class DiscountStrategyFactory(IEnumerable<IDiscountStrategy> strategies) : IDiscountStrategyFactory
{
public IDiscountStrategy GetStrategy(CustomerType customerType) => customerType switch
{
CustomerType.Regular => strategies.OfType<RegularDiscountStrategy>().Single(),
CustomerType.Premium => strategies.OfType<PremiumDiscountStrategy>().Single(),
_ => strategies.OfType<NoDiscountStrategy>().Single()
};
}
public class DiscountService(IDiscountStrategyFactory factory)
{
public double CalculateDiscount(CustomerType customerType, double price) =>
factory.GetStrategy(customerType).Calculate(price);
}
**Step 2b — the .NET 8+ way, using Keyed DI instead of hand-rolling a factory (mention this for extra credit — it shows you track the platform, not just the pattern from a textbook):**
// Program.cs
builder.Services.AddKeyedSingleton<IDiscountStrategy, RegularDiscountStrategy>(CustomerType.Regular);
builder.Services.AddKeyedSingleton<IDiscountStrategy, PremiumDiscountStrategy>(CustomerType.Premium);
public class DiscountService([FromKeyedServices(CustomerType.Regular)] IDiscountStrategy regular, ...)
// or, resolved dynamically at call time via IKeyedServiceProvider when the key isn't known until runtime:
public class DiscountService(IKeyedServiceProvider provider)
{
public double CalculateDiscount(CustomerType customerType, double price) =>
(provider.GetKeyedService<IDiscountStrategy>(customerType) ?? new NoDiscountStrategy())
.Calculate(price);
}
Notice what changed: customerType became an enum (fixes primitive obsession too), and adding
a VipDiscountStrategy next month means adding one new class and one new DI registration line —
DiscountService itself never changes again. That's Open/Closed in practice, made testable by DI
(mock IDiscountStrategyFactory or IDiscountStrategy directly in a unit test with zero database or
real strategy logic involved).