Course content
Strategy: Refactor the Checkout Discount Engine
Medium·Tagsdesign-patternsstrategycompositionrefactoring
Problem Statement
The starter `CheckoutService` accepts a `DiscountStrategy` in its constructor but does not actually delegate to it. Instead, `calculateTotal` does an `instanceof` chain on the strategy reference and computes the discount inline with hardcoded values:
```java
public double calculateTotal(List<Double> itemPrices) {
double subtotal = sum(itemPrices);
if (strategy instanceof PercentageDiscount) {
return subtotal * 0.9; // 10% off, hardcoded
} else if (strategy instanceof FlatDiscount) {
return Math.max(0, subtotal - 50); // flat $50, hardcoded
} else if (strategy instanceof BogoDiscount) {
... // BOGO logic inline
} else {
return subtotal;
}
}
```
This is the wrong way to use a strategy. The reference is being treated as a tag ("which branch do I take?") instead of as a delegate ("compute the discount for me"). It also makes the strategy parameters useless: `new PercentageDiscount(20)` produces the same result as `new PercentageDiscount(10)` because the hardcoded `0.9` ignores the field on the strategy.
Refactor so that the discount math lives in each strategy class and `CheckoutService.calculateTotal` becomes a one-line delegation. After your refactor, the design must expose this exact public API:
```java
interface DiscountStrategy {
double computeDiscount(List<Double> itemPrices);
}
class NoDiscount implements DiscountStrategy { ... }
class PercentageDiscount implements DiscountStrategy {
public PercentageDiscount(double percent);
}
class FlatDiscount implements DiscountStrategy {
public FlatDiscount(double amount);
}
class BogoDiscount implements DiscountStrategy { ... }
class CheckoutService {
public CheckoutService(DiscountStrategy strategy);
public void setDiscountStrategy(DiscountStrategy strategy);
public double calculateTotal(List<Double> itemPrices);
// delegates to strategy.computeDiscount(itemPrices); contains no instanceof.
}
```
The four `computeDiscount` methods on the strategy classes are stubbed in the starter (they all return 0). Move the discount math out of `CheckoutService` and into the right strategy class. Then replace the body of `calculateTotal` with subtotal minus `strategy.computeDiscount(itemPrices)`.
The validator runs ten checks that defeat the obvious cheats:
1. `new PercentageDiscount(10).computeDiscount(
[100, 200])` returns 30.0 when called directly. The percent logic must live in the strategy.
2. `new FlatDiscount(50).computeDiscount([100, 200])` returns 50.0.
3. `new FlatDiscount(500).computeDiscount([100, 200])` returns 300.0. Flat discount cannot exceed subtotal.
4. `new BogoDiscount().computeDiscount([10, 20, 30])` returns 20.0. Sort descending; items at odd indices (1, 3, ...) are free.
5. `new NoDiscount().computeDiscount([100, 200])` returns 0.0.
6. `new CheckoutService(new PercentageDiscount(20)).calculateTotal([100, 200])` returns 240.0. The validator deliberately uses 20% to defeat starter code that hardcodes 10%.
7. `setDiscountStrategy` actually swaps the strategy at runtime: same instance produces a different total after the swap.
8. The validator defines its own `DiscountStrategy` subclass (a 99%-off loyalty discount), injects it, and asserts the service uses it. Any `instanceof` chain on the strategy field fails this check.
9. The `CheckoutService` field that holds the strategy is declared as `DiscountStrategy` (the interface), not as a concrete class.
10. `CheckoutService` does not declare its own discount-computation methods (`applyPercentageDiscount`, `applyFlatDiscount`, `applyBogoDiscount`, etc.). The discount logic must live on the strategy classes.
The smelly starter passes only the constructor and reflection shape; it fails every behavioural check until the refactor is real.Examples
Example 1
Input
new PercentageDiscount(15).computeDiscount(List.of(200.0))Output
"30.0"Why
15% of 200 is 30. The strategy class owns the percent rule.
Example 2
Input
new CheckoutService(new BogoDiscount()).calculateTotal(List.of(10.0, 20.0, 30.0))Output
"40.0"Why
BOGO sorts descending [30, 20, 10] and frees items at odd indices: 20 is free. Total = 60 - 20 = 40.
Example 3
Input
service.setDiscountStrategy(new LoyaltyDiscount()); service.calculateTotal(prices);Output
"depends on LoyaltyDiscount.computeDiscount"Why
Adding a new discount type is a new class implementing DiscountStrategy. CheckoutService handles it without any change because it only depends on the interface.
Constraints
- •Refactor must keep these classes named: DiscountStrategy, NoDiscount, PercentageDiscount, FlatDiscount, BogoDiscount, CheckoutService.
- •DiscountStrategy must remain an interface with method `double computeDiscount(List<Double> itemPrices)`.
- •CheckoutService must accept a DiscountStrategy in its constructor and expose setDiscountStrategy(DiscountStrategy).
- •CheckoutService.calculateTotal must take only List<Double> and must not branch on the runtime type of its strategy field.
- •The strategy field on CheckoutService must be declared as DiscountStrategy (the interface), not a concrete class.