Open/Closed
Open for extension, closed for modification: add behavior without editing tested code, using polymorphism and the strategy pattern instead of growing if/else chains.
On this page
The Open/Closed Principle sounds like a paradox: software should be open for extension but closed for
modification. How do you add behavior without changing code? The answer is polymorphism - design so new
cases plug in as new classes rather than new branches in an existing method. It's the principle that stops the
growing if/else chain, the most common decay pattern in real code.
The growing switch smell
ledger-legacy calculates fees by transaction type, and it grows every time finance invents a new type:
BigDecimal calculateFee(Transaction t) {
if (t.type().equals("DOMESTIC")) return t.amount().multiply(new BigDecimal("0.01"));
else if (t.type().equals("INTL")) return t.amount().multiply(new BigDecimal("0.03"));
else if (t.type().equals("CRYPTO")) return t.amount().multiply(new BigDecimal("0.05"));
// ...a new 'else if' every quarter, and you must RE-EDIT and RE-TEST this method each time
else return BigDecimal.ZERO;
}Every new fee type means modifying a method that already works and is already tested - risking the existing
cases. This method is not closed for modification; it's a magnet for it. Each edit is a chance to break
DOMESTIC while adding CRYPTO.
Closing it with polymorphism
Invert it: define an abstraction for "a fee policy," and make each type its own implementation. Adding a type becomes adding a class, never touching the calculator:
interface FeePolicy {
boolean appliesTo(Transaction t);
BigDecimal fee(Transaction t);
}
class DomesticFee implements FeePolicy {
public boolean appliesTo(Transaction t) { return t.type() == Type.DOMESTIC; }
public BigDecimal fee(Transaction t) { return t.amount().multiply(new BigDecimal("0.01")); }
}
// IntlFee, CryptoFee ... each a small, independently-tested class
class FeeCalculator {
private final List<FeePolicy> policies; // injected - the strategy pattern
BigDecimal calculate(Transaction t) {
return policies.stream()
.filter(p -> p.appliesTo(t)).findFirst()
.map(p -> p.fee(t)).orElse(BigDecimal.ZERO);
}
}Now FeeCalculator is closed - you never edit it again - while the system is open: a new fee type is a
new FeePolicy class, added without risk to the tested ones. In Spring, injecting List<FeePolicy> wires every
implementation automatically, so registering a new one is truly zero edits to existing code.
The abstraction goes where change happens
OCP doesn't mean "make everything extensible" - that's the abstraction tax again. It means predict the axis of change and put the seam there. Fee types change often, so abstract over them. If the report format never varies, don't build a plugin system for it. The art is choosing which variation to design for; over-applying OCP buries simple code under interfaces for changes that never come.
OCP is the strategy pattern, mostly
The polymorphic fix above is the Strategy pattern: a family of interchangeable algorithms behind one interface, selected at runtime. Most OCP in practice is 'replace a conditional with strategy/polymorphism.' We'll go deeper on patterns next module - for now, notice that a design pattern is just a named, reusable way to satisfy a principle.
A method with a growing if/else is like a wall where every appliance is hard-wired directly into the mains - to add a lamp, an electrician opens the wall and risks the existing circuits. OCP is installing a power strip with sockets (the interface): a new lamp just plugs in, no wall-opening, no risk to the fridge already running. You didn't make the wall changeable - you made it extensible through a stable socket. And you only wire in sockets where you expect to add appliances, not on every square inch of wall.
An AlertService has a method with if (channel.equals("EMAIL")) {...} else if (channel.equals("SMS")) {...}
and product keeps asking for new channels (Slack, push, webhook), each requiring an edit and regression test of
this method. Describe the OCP refactor, and name one situation where you'd deliberately not apply it here.
How do you make code 'open for extension but closed for modification'?
Key takeaways
- Open/Closed: add new behavior by adding code (new implementations), not by modifying existing, tested code.
- A growing if/else or switch by 'type' is the classic OCP violation - every new case forces a risky edit to working code.
- Fix it with polymorphism: an interface for the varying behavior, one implementation per case - usually the Strategy pattern.
- In Spring, injecting a List of the interface auto-registers implementations, making a new case truly zero edits to existing classes.
- Apply OCP at the predicted axis of change; abstracting variation that never comes is speculative generality (YAGNI).