Design Patterns · Behavioral Patterns
Strategy
Put each version of an algorithm in its own class so the caller can swap them at runtime.
This lesson builds on Composition over inheritance. Strategy is that principle applied to one algorithm.
Strategy puts each version of an algorithm in its own class behind one shared interface. The object that needs the result holds one of those classes and calls it through the interface. It does not know which version it holds.
Strategy does not remove the decision about which version to run. It moves that decision out of the code that does the work. Someone still has to choose.
Start from a growing if-chain
A ride app quotes fares. The first release has one rule: a base fee plus a price per kilometer. Then the business adds surge pricing for busy hours. Then shared rides at a discount. Then a flat airport rate. Each rule lands in the same method.
type Trip = { km: number };
class FareCalculator {
calculate(trip: Trip, mode: string): number {
const standard = 2.5 + 1.2 * trip.km;
if (mode === "standard") return standard;
if (mode === "surge") return standard * this.surgeMultiplier();
if (mode === "shared") return Math.max(4, standard * 0.7);
if (mode === "airport") return this.airportFlatRate(trip);
throw new Error("Unknown pricing mode: " + mode);
}
private surgeMultiplier(): number { return 1.8; }
private airportFlatRate(_trip: Trip): number { return 25; }
}
This works for two rules. It decays as rules arrive:
- every new rule edits a method that already works, so every rule is at risk
- two engineers adding two rules edit the same lines and fight merge conflicts
- the class collects helpers and data that only one rule needs
- a test for the airport rate still runs through code built for surge pricing
A conditional on a type name is a missing abstraction
When a method switches on a mode string to pick a formula, each branch is already a separate algorithm. It just does not have a name or a class yet.
Check yourself
Every new pricing rule edits FareCalculator.calculate(). Which responsibility is varying?
Correct. Extract the changing algorithm so the quoting workflow can remain stable.
Not quite. A useful boundary isolates the actual variation, not the whole feature indiscriminately.
Move each rule into its own class
Strategy says: extract each branch into its own class, and make all of them implement one interface. The original class, now called the context, keeps a reference to one strategy and delegates to it. The context no longer knows any formula.
interface PricingStrategy {
price(trip: Trip): number;
}
class StandardPricing implements PricingStrategy {
price(trip: Trip) {
return 2.5 + 1.2 * trip.km;
}
}
class SurgePricing implements PricingStrategy {
constructor(private multiplier: number) {}
price(trip: Trip) {
return new StandardPricing().price(trip) * this.multiplier;
}
}
class SharedPricing implements PricingStrategy {
price(trip: Trip) {
return Math.max(4, new StandardPricing().price(trip) * 0.7);
}
}
class FareQuote {
constructor(private pricing: PricingStrategy) {}
setStrategy(pricing: PricingStrategy) {
this.pricing = pricing;
}
total(trip: Trip): number {
return this.pricing.price(trip); // no if-chain, no formula
}
}
The client, here a checkout screen, picks the strategy. It can change the strategy at runtime with setStrategy, for example when surge hours begin.
Try it
Swap the strategy. Watch the context stay the same.
Pick a pricing rule and a trip length. The call into FareQuote is identical every time.
quote.setStrategy(new StandardPricing())
quote.total({ km: 3 })
→ StandardPricing.price() runs
= $6.10
quote.setStrategy(new StandardPricing())
quote.total({ km: 12 })
→ StandardPricing.price() runs
= $16.90
quote.setStrategy(new SurgePricing(1.8))
quote.total({ km: 3 })
→ SurgePricing.price() runs
= $10.98
quote.setStrategy(new SurgePricing(1.8))
quote.total({ km: 12 })
→ SurgePricing.price() runs
= $30.42
quote.setStrategy(new SharedPricing())
quote.total({ km: 3 })
→ SharedPricing.price() runs
= $4.27
quote.setStrategy(new SharedPricing())
quote.total({ km: 12 })
→ SharedPricing.price() runs
= $11.83
Only the object behind the interface changed. FareQuote.total() has one line, and it never branches.
A function can be a strategy
In languages with first-class functions, a strategy is often just a function: type Pricing = (trip: Trip) => number. Use a class when the strategy carries its own state or dependencies, such as a surge multiplier loaded from a service.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Context | FareQuote | Holds a strategy reference and calls it through the interface |
| Strategy | PricingStrategy | Declares the one method every variant implements |
| Concrete strategies | StandardPricing, SurgePricing, SharedPricing | Each implements one version of the algorithm |
| Client | Checkout screen | Chooses a strategy and passes it to the context |
Step through
Trace one fare quote through the roles
1. The client chooses
Surge hours begin. The checkout screen creates new SurgePricing(1.8). Only the client knows a concrete class name.
2. The client configures the context
It calls quote.setStrategy(surge). The context stores the object in a field typed as the interface.
3. The context delegates
quote.total(trip) calls this.pricing.price(trip). The context cannot tell which class answers.
4. The concrete strategy does the work
SurgePricing.price runs its formula and returns a number. The context returns that number unchanged.
Reach for it when the variants keep coming
- an object needs different versions of one algorithm and must switch between them at runtime
- several classes are identical except for how they perform one behavior
- an algorithm brings data and dependencies that the business class should not carry
- a large conditional picks between versions of the same algorithm
Skip it when the algorithm almost never changes. Two branches that have been stable for a year do not need an interface and three classes.
Check yourself
One stable calculation has no alternate variants. Does Strategy automatically improve it?
Not quite. An unused extension point still has naming and maintenance costs.
Correct. A simple calculation can remain a function until variants or dependencies justify more.
Implement it in five steps
- Find the algorithm that changes often, or the conditional that picks between its versions.
- Declare a strategy interface with one method that every version can implement.
- Move each version into its own class that implements the interface.
- Give the context a field of the interface type and a setter. The context talks to the strategy only through the interface.
- Make the client choose a strategy and pass it to the context.
Check yourself
Where does the stable context keep the selected algorithm?
Not quite. That reintroduces the concrete selection logic into the context.
Correct. The context delegates without naming a concrete algorithm.
Name what it costs
| You gain | You pay |
|---|---|
| Swap algorithms at runtime | More classes and an interface, even for a choice that rarely changes |
| Algorithm code and data live apart from the code that uses them | The client must understand the differences between strategies to pick one |
| Composition replaces a subclass per variant | In languages with lambdas, a class per strategy can be ceremony |
| Add a strategy without editing the context (open/closed) | Every strategy must fit one method signature, even when a variant needs more input |
Check yourself
Who must understand the differences when choosing a pricing strategy?
Correct. Delegating the formula does not remove the decision about which formula applies.
Not quite. An interface defines operations. It does not choose the correct variant.
Do not confuse it with its neighbors
| Pattern | Looks like Strategy because | Differs because |
|---|---|---|
| State | A context delegates to a swappable helper | State represents lifecycle-dependent behavior and transitions, which states or the context can trigger. Strategies usually remain independent algorithm choices. |
| Template Method | It varies part of an algorithm | Its step implementations belong to a subclass. Clients can choose a subclass instance at runtime. Strategy instead replaces a collaborator on the context object. |
| Command | It wraps behavior in an object | Command turns any operation into an object you can queue, log, or undo. Strategy describes different ways to do the same job. |
| Bridge | The class diagram is nearly identical | Bridge splits a whole class into two hierarchies that grow independently. Strategy swaps one algorithm. |
| Decorator | It changes behavior without subclassing the object | Decorator changes an object's skin by wrapping it. Strategy changes its guts by replacing an internal piece. |
Check yourself
A helper represents Draft and transitions the document to Review. Which intent fits better than Strategy?
Correct. The helper expresses lifecycle behavior and transitions.
Not quite. There is no optional wrapper stack adding responsibilities.
Retrieve and apply
Answer without scrolling up. Then say the reason out loud, as you would in an interview.
Check yourself
A delivery app picks a route algorithm: fastest, cheapest, or fewest stops. After each leg, the current algorithm must decide which algorithm runs the next leg. Which pattern fits better?
Not quite. The algorithms must choose their successor, so they have to know about each other. Strategies stay independent of one another.
Correct. When the helper objects trigger transitions between themselves, the design is State. Strategy assumes an outside client does the choosing.
Not quite. Nothing here fixes a shared sequence of steps. The variation is which algorithm runs, chosen at runtime.
Check yourself
A report exporter supports CSV and nothing else, and no other format is planned. Should you introduce Strategy now?
Not quite. That pays for flexibility nobody asked for. Extract the interface when the second format is real. The refactor is small.
Correct. Strategy earns its cost when variants exist or keep arriving. One stable algorithm does not need it.
Continue to Template Method to see the inheritance-based way to vary only some steps of an algorithm.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Strategy, printed pages 345–356. Explanations, examples, and exercises are adapted for this course.