Open/Closed Principle
Adding without breaking
The Open/Closed Principle states that a module should be open for extension and closed for
modification. In practice this means: when a new requirement arrives, new code is added rather than
editing existing, tested code. Editing working code is a common source of regressions — a change
to one branch of a switch can break another. The goal is a structure where adding behavior does
not require modifying code that already works.
The principle is most visible in classes that branch on a category. A discount calculator, a shipping-cost estimator, a tax engine — any system where the list of categories grows over time — will show whether the design is open/closed or not.
The branching smell
A shipping-cost calculator starts with two carrier options and a simple if/elif. Six months
later it handles four carriers. A year later it handles eight, each with its own pricing rules.
At that point every new carrier requires opening the calculator, navigating the existing branches,
and verifying that no existing rule is accidentally affected. The class was intended to be stable,
but it requires modification each time the list of carriers grows.
1# Bad: adding a new carrier means editing this function and risking existing branches.
2def calculate_shipping(carrier: str, weight_kg: float) -> float:
3 if carrier == "standard":
4 return 2.50 + weight_kg * 0.30
5 elif carrier == "express":
6 return 8.00 + weight_kg * 0.75
7 elif carrier == "overnight":
8 return 20.00 + weight_kg * 1.50
9 elif carrier == "freight":
10 # Freight has a minimum charge and a per-100kg rate
11 base = max(50.0, weight_kg / 100 * 12.0)
12 return base
13 else:
14 raise ValueError(f"Unknown carrier: {carrier}")
The fix: a strategy you extend, not a branch you edit
The open design extracts the pricing rule into an abstraction — a ShippingCarrier interface —
and gives each carrier its own class that implements it. Adding a new carrier means writing a new
class. The calculator itself does not change; it only calls cost() on whatever carrier it
receives. Existing carrier classes are unaffected by a new addition because there is no shared
branching code to modify.