Open/closed principle
Classes should be open for extension but closed for modification.
The two halves sound like they cancel out. They do not, because they describe different activities. A class is open if you can build on it from the outside - subclass it, implement its interface, plug something new into it. A class is closed if it is complete: its interface is settled, other code relies on it, and nobody needs to reopen the file to add a feature.
Formal statement
A module is open/closed with respect to a given axis of variation if new variants along that
axis can be added by writing new code - a new class, a new implementation of an existing
interface - without editing a single line of the module's own source, and without recompiling
or redeploying its existing clients. "Closed" is always closed with respect to something:
Order stays closed against new shipping methods while remaining wide open to, say, a new
getTotalWeight() method nobody anticipated. Pick the axis worth protecting; you cannot
close a class against every possible future change, and trying to produces the
over-abstracted version of this principle described below.
The point of holding both halves at once is simple: new features should not be able to break old ones. If shipping a new requirement means editing a class that is already developed, tested, reviewed and depended upon, you are gambling with code that was working an hour ago.
The example
An e-commerce Order class calculates shipping costs, and every shipping method lives inside
it as a branch of an if. Ground shipping has a free-delivery threshold, air shipping is a
flat rate, each has its own delivery estimate. It works, right up until sales promises
customers same-day delivery.
Now adding a shipping method means editing Order, which is the class that also computes
totals, and which every checkout path in the application runs through. The risk is entirely
disproportionate to the feature.
Extract shipping methods into their own classes behind a common Shipping interface, and the
new method arrives as a new class. Order is not touched. Client code links the order to
whichever shipping object the user picked in the UI, and that is the only place that knows
concrete types.
Order.Order never moves.Two things worth noticing. First, this is the Strategy pattern, again arrived at by following a principle rather than reaching for a catalog. Second, moving the delivery-time calculation next to the cost calculation also fixed a single responsibility problem for free - the principles tend to pull in the same direction.
A second example
A Checkout class calculates sales tax with an if per country - no VAT logic at all for
the US, a flat 20% for the UK, 19% for Germany. It is the same shape as the shipping example
in a completely different domain: finance instead of logistics, but still one class growing
a new branch every time the business expands into a market.
Extracting a TaxRule interface with one implementing class per jurisdiction means launching
in a new country is a new TaxRule, reviewed and tested on its own, dropped into Checkout
through the same constructor parameter every other jurisdiction already uses.
The smell
A conditional - if/else if or switch - branching on a type code (a country string, a
shipping-method string, an enum tag), where each arm is a self-contained rule and the list of
arms has grown at least once since the code was written. The tell is in the git history more
than the code itself: a file that gets a new commit every time the business adds a variant is
the principle's target, not a stylistic complaint about if statements in general.
Patterns that satisfy it
This is the principle Strategy and Decorator both exist to serve. Strategy swaps the whole algorithm behind one interface (shipping cost calculation, tax calculation) so a new variant is a new class. Decorator adds behavior around an existing object without touching its class, which is open/closed applied to composition instead of branching logic - wrap an object in a new decorator instead of editing it to support a new combination of features.
Where it goes wrong
The principle is not a ban on editing classes.
- Bugs get fixed in place. If a class is wrong, correct it. Creating a subclass to route around a defect makes the child responsible for the parent's mistakes, and now you have two problems and a confusing hierarchy.
- Speculative extension points are a tax. Every seam you carve out costs indirection: one more interface to name, one more file to open, one more hop when reading a stack trace. Pay it where variation is plausible, not everywhere.
- Two variants is not always a pattern. Ground and air, with no third on the roadmap and
no team asking, is a perfectly respectable
if. Extract on the second or third genuine change request, when the direction of variation is known rather than guessed.
The tell that you actually need this: a conditional that has grown one arm per variant, and a
history of commits that all touch it. The honest cost of extracting too early is a codebase
of one-implementation interfaces - Shipping with only Ground, TaxRule with only
FlatVAT - which is indirection sold as flexibility nobody has used yet.
Try it yourself: Extract Order's getShippingCost()/getShippingDate() if/else chain
into a Shipping interface with one implementing class per shipping method, then rewrite
Order so adding same-day delivery never requires touching Order again.
Check yourself
How can one class be open and closed at the same time?