Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Strategy
Define a family of interchangeable algorithms behind one interface and choose one at runtime, so the code that uses them never needs an if/else to pick.
The problem
Shipping cost depends on the method the customer picks: standard (a flat fee, free over 50), express (a base fee plus per-kilogram), or store pickup (free). The shipping_cost() function is an if/elif chain mixed with each rule's details. Marketing wants a "free shipping weekend" rule, a partner wants their own rates, and tests have to go through the whole function to check one rule.
You saw the shape of the fix in the first lesson of this course: each variant becomes its own object.
Real-world analogy
Getting to the airport: you can drive, take the train, or call a taxi. The goal is the same and the interface is the same ("get me there by 10:00"), but the strategy differs. You choose based on budget and time, and you can switch strategy without changing the goal.
Without the pattern
def shipping_cost(method, weight_kg, subtotal):
if method == "standard":
return 0 if subtotal >= 5000 else 499
elif method == "express":
return 999 + 200 * weight_kg
elif method == "pickup":
return 0
# every new rule edits this function
The pattern
- Strategy interface: one method for the algorithm (
cost(weight, subtotal)). - Concrete strategies: one class (or one function) per algorithm.
- Context (
Checkout): holds a strategy and calls it. It never checks which strategy it has. - Client: picks the strategy, for example from user input or configuration, often through a dictionary lookup.
In Python and TypeScript, a strategy is often just a function. Java's functional interfaces and lambdas, and PHP closures, do the same; a full class is only needed when the strategy has configuration or several methods.
The code
class Standard:
def cost(self, weight_kg, subtotal):
return 0 if subtotal >= 5000 else 499
class Express:
def __init__(self, per_kg):
self.per_kg = per_kg # strategies can carry configuration
def cost(self, weight_kg, subtotal):
return 999 + self.per_kg * weight_kg
class Pickup:
def cost(self, weight_kg, subtotal):
return 0
class Checkout: # context: uses whatever strategy it is given
def __init__(self, shipping):
self.shipping = shipping
def total(self, weight_kg, subtotal):
return subtotal + self.shipping.cost(weight_kg, subtotal)
STRATEGIES = {"standard": Standard(), "express": Express(per_kg=200), "pickup": Pickup()}
for choice in ["standard", "express", "pickup"]:
total = Checkout(STRATEGIES[choice]).total(weight_kg=3, subtotal=4200)
print(f"{choice:<8} total: {total} cents")
# A strategy can also be a plain function wrapped in a tiny object
weekend = type("Weekend", (), {"cost": lambda self, w, s: 0})()
print(f"{'weekend':<8} total: {Checkout(weekend).total(3, 4200)} cents")
Output
standard total: 4699 cents express total: 5799 cents pickup total: 4200 cents weekend total: 4200 cents
interface ShippingStrategy {
cost(weightKg: number, subtotal: number): number;
}
class Standard implements ShippingStrategy {
cost(_w: number, subtotal: number) { return subtotal >= 5000 ? 0 : 499; }
}
class Express implements ShippingStrategy {
constructor(private perKg: number) {} // strategies can carry configuration
cost(weightKg: number, _s: number) { return 999 + this.perKg * weightKg; }
}
class Pickup implements ShippingStrategy {
cost() { return 0; }
}
class Checkout { // context: uses whatever strategy it is given
constructor(private shipping: ShippingStrategy) {}
total(weightKg: number, subtotal: number) { return subtotal + this.shipping.cost(weightKg, subtotal); }
}
const strategies: Record<string, ShippingStrategy> = {
standard: new Standard(), express: new Express(200), pickup: new Pickup(),
};
for (const choice of ["standard", "express", "pickup"]) {
const total = new Checkout(strategies[choice]).total(3, 4200);
console.log(`${choice.padEnd(8)} total: ${total} cents`);
}
// Structural typing: any object literal with cost() is a strategy
const weekend: ShippingStrategy = { cost: () => 0 };
console.log(`${"weekend".padEnd(8)} total: ${new Checkout(weekend).total(3, 4200)} cents`);
Output
standard total: 4699 cents express total: 5799 cents pickup total: 4200 cents weekend total: 4200 cents
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, ShippingStrategy> strategies =
Map.of("standard", new Standard(), "express", new Express(200), "pickup", new Pickup());
for (String choice : List.of("standard", "express", "pickup")) {
int total = new Checkout(strategies.get(choice)).total(3, 4200);
System.out.println(String.format("%-8s total: %d cents", choice, total));
}
// A functional interface lets a lambda be a strategy
ShippingStrategy weekend = (weightKg, subtotal) -> 0;
System.out.println(String.format("%-8s total: %d cents", "weekend", new Checkout(weekend).total(3, 4200)));
}
}
@FunctionalInterface
interface ShippingStrategy {
int cost(int weightKg, int subtotal);
}
class Standard implements ShippingStrategy {
public int cost(int weightKg, int subtotal) { return subtotal >= 5000 ? 0 : 499; }
}
class Express implements ShippingStrategy {
private final int perKg; // strategies can carry configuration
Express(int perKg) { this.perKg = perKg; }
public int cost(int weightKg, int subtotal) { return 999 + perKg * weightKg; }
}
class Pickup implements ShippingStrategy {
public int cost(int weightKg, int subtotal) { return 0; }
}
class Checkout { // context: uses whatever strategy it is given
private final ShippingStrategy shipping;
Checkout(ShippingStrategy shipping) { this.shipping = shipping; }
int total(int weightKg, int subtotal) { return subtotal + shipping.cost(weightKg, subtotal); }
}
Output
standard total: 4699 cents express total: 5799 cents pickup total: 4200 cents weekend total: 4200 cents
Run this on your computer — see “Get Started” for setup.
<?php
interface ShippingStrategy {
public function cost(int $weightKg, int $subtotal): int;
}
class Standard implements ShippingStrategy {
public function cost(int $weightKg, int $subtotal): int { return $subtotal >= 5000 ? 0 : 499; }
}
class Express implements ShippingStrategy {
public function __construct(private int $perKg) {} // strategies can carry configuration
public function cost(int $weightKg, int $subtotal): int { return 999 + $this->perKg * $weightKg; }
}
class Pickup implements ShippingStrategy {
public function cost(int $weightKg, int $subtotal): int { return 0; }
}
class Checkout { // context: uses whatever strategy it is given
public function __construct(private ShippingStrategy $shipping) {}
public function total(int $weightKg, int $subtotal): int { return $subtotal + $this->shipping->cost($weightKg, $subtotal); }
}
$strategies = ["standard" => new Standard(), "express" => new Express(200), "pickup" => new Pickup()];
foreach (["standard", "express", "pickup"] as $choice) {
$total = (new Checkout($strategies[$choice]))->total(3, 4200);
printf("%-8s total: %d cents\n", $choice, $total);
}
// An anonymous class is a quick one-off strategy
$weekend = new class implements ShippingStrategy {
public function cost(int $weightKg, int $subtotal): int { return 0; }
};
printf("%-8s total: %d cents\n", "weekend", (new Checkout($weekend))->total(3, 4200));
Output
standard total: 4699 cents express total: 5799 cents pickup total: 4200 cents weekend total: 4200 cents
Checkout has no idea how shipping is calculated. Each rule can be tested on its own, and the weekend promotion was added without editing anything.
When to use it
- You have several ways to do the same thing (pricing, sorting, compression, routing, payment, authentication, retry policies) and want to choose at runtime or by configuration.
- An
if/elseorswitchpicks an algorithm and keeps growing. - You want to test each algorithm in isolation.
When not to use it
- There are only two simple variants that rarely change. An
ifis fine. - In languages with first-class functions, a function parameter is often enough. You don't need an interface plus a class per strategy.
- Clients must know the strategies to choose between them. If choosing is complex, combine Strategy with a factory or a lookup table.
You've already used it
- Sorting with comparators:
sorted(items, key=...),array.sort((a, b) => ...),Comparatorin Java,usort()in PHP. - Passport.js and Laravel guards: authentication strategies (password, OAuth, JWT) behind one interface.
- Cache and retry policies in HTTP clients, and compression codecs chosen by configuration.
Key points
- Strategy puts interchangeable algorithms behind one interface; the context uses one without knowing which.
- It replaces algorithm-selecting
if/elsechains and makes each algorithm testable. - In Python, TypeScript, modern Java and PHP, a strategy is often just a function or lambda.
- Further reading: Refactoring Guru: Strategy.
Practice questions
What is the Strategy pattern? Give a real-world example.
Answer
Strategy defines a family of interchangeable algorithms behind one interface; the context holds one and delegates to it, so algorithms can be swapped at runtime without if/else. Examples: shipping cost rules, payment methods, comparators passed to a sort function, authentication strategies in Passport.js.
How is the Strategy pattern usually implemented in Python or TypeScript?
Answer
Often with plain functions: pass a function (key= in sorted, a comparator in array.sort) instead of defining an interface and a class per algorithm. Use classes when a strategy needs configuration or several related methods.
How does Strategy support the Open/Closed Principle?
Answer
New behaviour is added by writing a new strategy and registering it; the context that uses strategies is never edited. The code is open for extension and closed for modification.
How do you avoid a new if/else just to choose the strategy?
Answer
Use a lookup table (a map from name to strategy), configuration, or a factory. The selection then lives in one place, often at the composition root, instead of inside business logic.
Open in the app to mark lessons complete, take notes and track your progress.