BehavioralGoF classic
Strategy Pattern
Define a family of algorithms, put each in its own class, and make them interchangeable at runtime without changing the code that uses them.
1. Problem & the anti-pattern
Violation: Open/closed principleA checkout computes discounts. Every new promotion adds another branch to one growing conditional, so each change risks breaking the others and nothing can be tested alone.
Anti-pattern: Every rule edits one conditional
Adding a promotion means editing the same function, so every change can break every other rule.
function total(cart: Cart, promo: string) {
if (promo === 'PERCENT10') return cart.sum * 0.9;
else if (promo === 'FLAT50') return Math.max(0, cart.sum - 50);
else if (promo === 'BOGO') { /* … */ }
return cart.sum;
}
2. Structure & UML
Interactive schematicClass composition and runtime delegation.
100%
Checkout
- - strategy: DiscountStrategy
- + setStrategy(s)
- + total(cart): number
«interface»DiscountStrategy
- + apply(total): number
PercentOff
- + apply(total): number
FlatOff
- + apply(total): number
3. Structural participants
| Architectural role | Responsibility |
|---|---|
| Strategy | The common interface every algorithm implements (DiscountStrategy). |
| Concrete strategies | One class per algorithm (PercentOff, FlatOff). |
| Context | Holds a strategy and delegates to it (Checkout). It never knows which concrete class it has. |
4. Code implementation
3 languagesinterface DiscountStrategy {
apply(total: number): number;
}
class PercentOff implements DiscountStrategy {
constructor(private percent: number) {}
apply(total: number) {
return Math.floor((total * (100 - this.percent)) / 100);
}
}
class Checkout {
constructor(private strategy: DiscountStrategy) {}
setStrategy(s: DiscountStrategy) {
this.strategy = s;
}
total(prices: number[]) {
return this.strategy.apply(prices.reduce((a, b) => a + b, 0));
}
}
from typing import Protocol
class DiscountStrategy(Protocol):
def apply(self, total: int) -> int: ...
class PercentOff:
def __init__(self, percent: int):
self.percent = percent
def apply(self, total: int) -> int:
return total * (100 - self.percent) // 100
class Checkout:
def __init__(self, strategy: DiscountStrategy):
self.strategy = strategy
def total(self, prices: list[int]) -> int:
return self.strategy.apply(sum(prices))
type DiscountStrategy interface {
Apply(total int) int
}
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(total int) int { return total * (100 - p.Percent) / 100 }
type Checkout struct{ Strategy DiscountStrategy }
func (c *Checkout) Total(prices []int) int {
sum := 0
for _, p := range prices {
sum += p
}
return c.Strategy.Apply(sum)
}
5. Trade-offs
Key advantages
- Open/closed: new discounts are new classes; Checkout never changes.
- Testable in isolation: each rule is a tiny class with one method to test.
- Swappable at runtime: pick the rule per customer, campaign or A/B bucket.
Liabilities & overhead
- More classes: a two-branch if is simpler than two strategy classes.
- Callers must choose: someone still decides which strategy to pass in.
- Premature for one rule: an interface with a single implementation is noise.
6. When not to use
Premature abstraction
If the rule has one or two stable variants and no one asks to add more, keep the conditional. Extract a strategy the moment a third variant or a runtime switch appears.
Rule of thumb: three distinct algorithms, or one that changes at runtime.
7. Related patterns
8. Practical blueprint challenges
View all problemsNo problems use this pattern yet.