Open/Closed Principle
Add new behaviour by adding code, not by editing code that already works.
Big idea
The Open/Closed Principle (the O in SOLID): software should be open for extension, closed for modification. When a new case arrives — a new discount, a new file format — you should be able to add a new class or function and leave the tested, deployed code alone.
The problem it solves
A DiscountService has grown a ladder:
if customer.kind == "student": ...
elif customer.kind == "senior": ...
elif customer.kind == "employee": ...
Every new promotion edits this function. Each edit risks breaking the earlier cases, needs the whole function re-tested, and lands as a merge conflict with whoever is adding the other promotion this sprint.
Think of it like
A power strip. You add a fan by plugging it in — you don't rewire the strip. The strip is closed (nobody opens it up) and open (anything with the right plug works). The plug shape is the abstraction that makes both true.
How it works
Find the part that varies, put it behind an abstraction, and make the stable code depend only on that abstraction. New variants are new implementations, registered from the outside.
In code
class StudentRule:
def applies(self, cart): return cart["customer"] == "student"
def discount(self, cart): return 0.10 * cart["subtotal"]
class BulkRule:
def applies(self, cart): return cart["items"] >= 10
def discount(self, cart): return 5.0
class Checkout: # closed: never edited for new rules
def __init__(self, rules):
self.rules = list(rules)
def price(self, cart):
best = max((r.discount(cart) for r in self.rules if r.applies(cart)), default=0)
return round(cart["subtotal"] - best, 2)
cart = {"customer": "student", "items": 12, "subtotal": 80.0}
print(Checkout([StudentRule(), BulkRule()]).price(cart))
class FestivalRule: # open: a new class, added from outside
def applies(self, cart): return True
def discount(self, cart): return 0.15 * cart["subtotal"]
print(Checkout([StudentRule(), BulkRule(), FestivalRule()]).price(cart))output
72.0
68.0Run and edit it in the app.
FestivalRule arrived and Checkout didn't change — no retest of its logic, no conflict with the next rule.
What does "closed for modification" mean in practice?
Watch it
You can't close against everything
Closure is always with respect to some kind of change. Checkout above is closed against new discount rules, but not against "discounts now stack". Guess where variation will come from — or better, wait for the second or third real case, then refactor to open that axis. Abstracting every axis up front is speculative generality (YAGNI).
When to use it — and when not to
Apply it where new variants arrive regularly (payment methods, export formats, pricing rules, plugins). Leave simple code simple where the cases are few and stable; a two-branch if that hasn't changed in a year doesn't need a plugin system.
Pros and cons
| Pros | Cons |
|---|---|
| New features don't destabilise old ones | Needs an abstraction, which costs indirection |
| Parallel work: each variant is its own file | Guessing the wrong axis wastes effort |
| Old tests stay valid | Variants scattered across many small classes |
How it relates
- Polymorphism is the mechanism; Strategy, Decorator, Observer, and Chain of Responsibility are ready-made open/closed shapes.
- It depends on Liskov Substitution: a plug-in that breaks the contract breaks the closed code.
- The smell Switch Statements is the classic violation.
Checkout accepts any number of DiscountRule objects. Which new requirement still forces you to edit Checkout?
Key terms
- Open/Closed Principle — Code should accept new behaviour through extension, without editing what already works.
- Extension point — The abstraction where new variants plug in.
- Axis of change — A direction in which requirements tend to vary, such as "new payment method".