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 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.
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.
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.
# 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.
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?
OOP & design patterns