Open / Closed
A price calculator has an if/elif for every kind of discount.
Put the changing part behind an interface.
A switch or if/elif grows each time a new type appears
The picture from life
A power strip: when you buy a new gadget you plug it in. You do not break the wall and rewire the house. The strip is "closed" (never opened up) yet "open" (any new plug fits).
The problem
What goes wrong when you do not use it.
A price calculator has an if/elif for every kind of discount. Each new offer means editing the same function, retesting every old branch and risking a typo in code that already worked.
- calls / uses
- A switch or if/elif grows each time a new type appears
- You keep editing tested code to add a feature
- Several teams add variants of the same behaviour
The idea
How the pattern fixes it.
Put the changing part behind an interface. The calculator talks only to the interface; a new offer is a new class that implements it. Nothing old is touched.
- implements
- calls / uses
Who is who
- Discount (interface)
- The promise: apply(amount) returns the new price.
- FestivalDiscount, MemberDiscount...
- Each offer is one small class. Adding one never edits the others.
- checkout
- The closed code. It just calls discount.apply and never changes.
Build it in steps
- 1Find the thing that keeps changing (the type of discount).
- 2Define a small interface for it.
- 3Move each branch into its own class.
- 4Make the caller depend only on the interface.
Watch it run
Step through the calls. Tap an arrow to jump to it.
Marketing added StudentDiscount. checkout() was not edited at all.
checkout(1000, StudentDiscount())The code
Python and Java. Try writing the classes yourself from the diagram first.
class Discount:
def apply(self, amount):
return amount
class FestivalDiscount(Discount):
def apply(self, amount):
return amount * 0.9
class MemberDiscount(Discount):
def apply(self, amount):
return amount - 50 if amount > 500 else amount
class StudentDiscount(Discount): # added later, nothing else edited
def apply(self, amount):
return amount * 0.8
def checkout(amount, discount):
return discount.apply(amount) # closed for modification
def demo():
return checkout(1000, FestivalDiscount()), checkout(1000, MemberDiscount()), checkout(1000, StudentDiscount())What you gain
- New behaviour arrives as new code, not edits to old code
- Old features cannot be broken by a new one
What it costs
- You must guess the right place for the interface; guess wrong and it is clutter
- Do not add it before a second variant exists
You have already met it in
- Plugins in editors and browsers
- Sort with a comparator you pass in
Often mixed up with
- StrategyStrategy is the pattern that most often delivers Open/Closed. OCP is the goal, Strategy is one way to get there.
Make it stick
Write a shipping-cost function with three methods using if/else, then refactor so adding a fourth method needs no edit to old code.