The Strategy pattern — Open/Closed in practice
The design pattern that implements Open/Closed for real — a family of interchangeable algorithms, selected at runtime, with no branching logic anywhere.
3 min read
The problem it solves — the same one from the SOLID lesson
# The Open/Closed violation from the SOLID lesson
def apply_discount(order, discount_type):
if discount_type == "percentage":
order.total *= 0.9
elif discount_type == "flat":
order.total -= 10
# every new discount type means editing this function, risking every existing branchStrategy is the concrete pattern for fixing exactly this: instead of one function branching on a type flag, define a common interface for "a thing that can be applied," and make each variant its own class implementing that interface.
The pattern
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, total: float) -> float: ...
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent: float):
self.percent = percent
def apply(self, total: float) -> float:
return total * (1 - self.percent / 100)
class FlatDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def apply(self, total: float) -> float:
return max(0, total - self.amount)
class Order:
def __init__(self, total: float, discount: DiscountStrategy):
self.total = total
self.discount = discount
def final_total(self) -> float:
return self.discount.apply(self.total)
order = Order(100, PercentageDiscount(10))
order.final_total() # 90.0
order = Order(100, FlatDiscount(15))
order.final_total() # 85.0Order never asks "what kind of discount is this?" — it just calls .apply() on whatever DiscountStrategy it was given. Adding LoyaltyDiscount is writing a new class; Order and every existing strategy class stay completely untouched, which is exactly what "closed for modification" means in practice.
Why this is worth recognizing, not just applying
The giveaway that a Strategy pattern belongs somewhere is a conditional selecting between algorithms that all take the same inputs and produce the same kind of output — several ways to compute a price, several ways to sort a list, several ways to validate input, several ways to format a report. If the branches are each doing meaningfully different, unrelated things (not variations on "compute a total"), forcing them into a shared interface is over-engineering, not Strategy.
The real trade-off
# Before: one function, all logic visible in one place
def apply_discount(order, discount_type): ...
# After: behavior visible only by opening each strategy class
class PercentageDiscount(DiscountStrategy): ...
class FlatDiscount(DiscountStrategy): ...The if/elif version has a real advantage the Strategy version gives up: every case is visible in one place, in one function, readable top to bottom without navigating between files. Strategy trades that single-file readability for extensibility without risk to existing cases. This is worth being honest about rather than treating Strategy as a strictly superior default — for a discount system with two fixed, rarely-changing cases that will never grow, the plain if/elif might genuinely be the better, simpler choice. Strategy earns its keep specifically when new cases get added often enough, or independently enough (by different people, at different times), that "editing a shared function every time" becomes the actual bottleneck.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. In the Strategy pattern, how does Order decide which discount logic to run?
2. What's the giveaway that a problem is a good fit for the Strategy pattern?
3. What real advantage does the plain if/elif version have that Strategy gives up?
4. When does Strategy 'earn its keep' according to the lesson, rather than being unnecessary complexity?