← All topics
🧩 Design Patterns

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).

Flashcards (6)

Name the two concrete code smells in the original DiscountService before refactoring.
tap to reveal answer
1) Open/Closed violation — adding a customer type means editing the if/else chain. 2) Primitive obsession — customerType is a raw string, so a typo compiles fine and silently returns 0.
In the Strategy pattern refactor, what's the job of the Factory, precisely?
tap to reveal answer
To translate the input (customer type) into the correct IDiscountStrategy implementation, so DiscountService itself only ever depends on the IDiscountStrategy abstraction, never on concrete strategy classes or the branching logic.
How would you add support for a new 'VIP' customer type after this refactor, and what does NOT need to change?
tap to reveal answer
Add a new VipDiscountStrategy class implementing IDiscountStrategy, and one new DI registration line. DiscountService and DiscountStrategyFactory's structure don't need to change — that's the Open/Closed payoff.
What .NET 8 feature can replace a hand-rolled IDiscountStrategyFactory, and what's the registration call?
tap to reveal answer
Keyed DI Services — register each strategy with AddKeyedSingleton<IDiscountStrategy, RegularDiscountStrategy>(key), then resolve by key via [FromKeyedServices(key)] or IKeyedServiceProvider.GetKeyedService<T>(key) at runtime.
Why is the refactored version more testable than the original?
tap to reveal answer
DiscountService now depends on an injected interface (IDiscountStrategyFactory), so a unit test can supply a mock/fake factory returning a test double strategy — no need to exercise real discount math or any concrete strategy to test DiscountService's own logic in isolation.
Why did customerType change from string to an enum as part of this refactor, and which principle/smell does that address?
tap to reveal answer
Clean Code's primitive obsession smell — a string has no compiler-enforced valid set of values, so a typo silently falls through to the default case. An enum makes invalid values a compile error instead of a silent runtime bug.