Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles
The Open/Closed Principle works because a stable abstraction (an interface or abstract base) lets the caller dispatch to behavior chosen at runtime by the object's concrete type, so a new behavior is added by writing a new class the caller already knows how to invoke — never by re-opening and re-editing the caller. "Open for extension, closed for modification" is not a slogan about willpower; it is a structural consequence of routing calls through a polymorphic boundary instead of a conditional.
The mechanism, traced on a real pricing engine
Take a checkout that applies a discount. The naive version branches on a string tag, so every new promotion forces an edit to the one method everyone depends on:
// VIOLATES OCP — every new promo re-opens this method
double priceAfterDiscount(String kind, double total) {
if (kind.equals("NONE")) return total;
else if (kind.equals("FLAT10")) return total - 10;
else if (kind.equals("PCT20")) return total * 0.80;
// add BLACK_FRIDAY? edit here, re-test the whole method...
throw new IllegalArgumentException(kind);
}The OCP version pushes each rule behind one stable abstraction. The caller is now closed — adding BlackFriday touches zero existing lines:
interface DiscountPolicy { // the stable boundary
double apply(double total);
}
class NoDiscount implements DiscountPolicy {
public double apply(double total) { return total; }
}
class FlatOff implements DiscountPolicy {
private final double amount;
FlatOff(double amount) { this.amount = amount; }
public double apply(double total) { return Math.max(0, total - amount); }
}
class PercentOff implements DiscountPolicy {
private final double pct; // 0.20 == 20% off
PercentOff(double pct) { this.pct = pct; }
public double apply(double total) { return total * (1 - pct); }
}
// CLOSED: this never changes when a new policy is added
class Checkout {
double priceAfterDiscount(DiscountPolicy policy, double total) {
return policy.apply(total);
}
}Go, identical shape — a small interface and value structs:
type DiscountPolicy interface{ Apply(total float64) float64 }
type NoDiscount struct{}
func (NoDiscount) Apply(t float64) float64 { return t }
type FlatOff struct{ Amount float64 }
func (f FlatOff) Apply(t float64) float64 {
if d := t - f.Amount; d > 0 { return d }
return 0
}
type PercentOff struct{ Pct float64 } // 0.20 == 20% off
func (p PercentOff) Apply(t float64) float64 { return t * (1 - p.Pct) }
func PriceAfterDiscount(p DiscountPolicy, total float64) float64 {
return p.Apply(total)
}Why the naive version is wrong: it is not just ugly, it is a correctness and risk hazard. The if/else chain is a single shared mutation point — every team adding a promo edits the same method, re-tests every existing branch, and risks a typo in an unrelated case (forget the final throw and an unknown tag silently returns nothing). The polymorphic version makes the branch structural: the JVM/Go runtime selects the implementation by type, so a wrong policy is a compile-time type error, not a runtime fall-through.
Step-by-step dispatch trace
Call checkout.priceAfterDiscount(new PercentOff(0.20), 250.0):
| Step | What happens | Value |
|---|---|---|
| 1 | Caller holds a DiscountPolicy reference; static type is the interface | ref → PercentOff{pct=0.20} |
| 2 | Runtime reads the object's vtable/itable, resolves apply to PercentOff.apply | dispatch target fixed |
| 3 | Body runs total * (1 - pct) | 250.0 * 0.80 |
| 4 | Returns to the unchanged Checkout | 200.0 |
Now ship a Black Friday rule. You add one class and wire it where the policy is constructed (a factory or config) — the highlighted boundary below is the only code that ever learns the new name; Checkout is untouched:
class BlackFriday implements DiscountPolicy { // NEW FILE
public double apply(double total) {
double afterPct = total * 0.70; // 30% off
return afterPct > 200 ? afterPct - 25 : afterPct; // +$25 over $200
}
}
// apply(250.0): 250*0.70 = 175.0; 175 > 200? no → 175.0Pitfalls a working engineer hits
- Speculative extension points. Building
DiscountPolicybefore there is a second discount is YAGNI debt: you pay indirection now for a variation that may never arrive. The honest trigger is the second reason to change, not the first. - The leaky abstraction. If
Checkoutever doesif (policy instanceof BlackFriday), the boundary is fake — you have re-introduced the switch and OCP is gone. Anyinstanceof/type-switch on the policy type is the smell. - Moving the switch, not removing it. Something must still pick the concrete policy. If that selection lives in the same module as
Checkout, you only relocated the conditional. Push construction to a factory, DI container, or config so the closed code never names concrete types. - Over-abstraction kills readability. Five files and an interface to express what one
total * 0.8line did is a net loss when the rule is genuinely fixed. OCP earns its keep only along an axis that actually varies. - LSP violation breaks it silently. If a new policy returns a negative price or throws where others don't, the closed caller breaks even though it compiled. OCP assumes every implementation honors the same contract — that assumption is exactly the Liskov Substitution Principle.
When to reach for OCP — and when not to
The decision is really polymorphic dispatch vs. a conditional. Both produce correct prices; they differ in who pays the maintenance cost.
Signals that point to OCP (the interface): the set of variants is open-ended and grows over time; different variants are owned by different teams/plugins; you want each rule independently unit-tested and shippable; the variation is a first-class business concept ("a promotion") worth a name.
Signals that point to a plain switch / if-else: the cases are fixed and few (an enum of three you control); the logic per case is one line; everything lives in one module; the cases must be handled exhaustively and you want the compiler to flag a missing one (a sealed-type switch gives you that — an open interface does not).
Trade-offs vs. the conditional: OCP buys you a closed, never-re-tested core and pluggable variants; it costs an extra type per variant, a layer of indirection that obscures control flow in a stack trace, scattered logic (you can no longer read all rules in one place), and a virtual-call instead of an inlined branch. The conditional is denser and faster to read for a fixed set, but every new case re-opens shared code.
One-liner: choose OCP polymorphism when variants are open-ended and independently owned; prefer a switch (ideally over a sealed type) when the cases are a small fixed set you control and want exhaustively checked.
Concrete call: for the three built-in discounts above, a sealed-type switch is fine. The moment marketing files "let third-party partners ship their own promo logic," the variant set is now open and partner-owned — that is the signal to commit to the DiscountPolicy boundary.
How OCP actually rests on the other SOLID principles
The relation is not decorative — OCP is load-bearing on three of the others, and the pricing example shows each:
- SRP gives you the seam. You can only swap a discount independently because "how to discount" was separated from "how to check out." Each policy class has one reason to change (its own rule);
Checkouthas one reason to change (the checkout flow). Without that single-responsibility split there is no clean axis to extend along — the variation and the orchestration would be tangled in one method, exactly the naive version. - LSP guarantees the extension is safe. The closed caller trusts that any
DiscountPolicyreturns a sane, non-negative price. OCP is only sound if every subtype is truly substitutable. - DIP is the wiring that keeps it closed.
Checkoutdepends on theDiscountPolicyabstraction, not onPercentOff; the concrete choice is injected from outside. That inversion is what stops the concrete-type names from leaking back into the closed code.
So the chain reads: SRP carves the seam → DIP points the caller at the abstraction across it → LSP keeps every implementation honest → and the emergent property is OCP: extend by adding, never by editing.
Takeaways
- OCP is the structural payoff of routing a call through a polymorphic boundary: the runtime picks behavior by type, so a new behavior is a new class, not an edit to the caller.
- It is not free — earn it on an axis that genuinely varies and is open-ended; for a small fixed set, a switch (ideally over a sealed type, so the compiler checks exhaustiveness) is the better engineering call.
- The instant you write
instanceofon the abstraction, or your selection logic lives next to the closed code, OCP is already broken. - OCP doesn't stand alone: SRP supplies the seam, DIP wires the caller to the abstraction, and LSP keeps every extension substitutable.
Re-authored and deepened for this guide. Sources: Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (the origin of the SOLID acronym) and Clean Architecture; Bertrand Meyer, Object-Oriented Software Construction (the original Open/Closed formulation); Barbara Liskov & Jeannette Wing, "A Behavioral Notion of Subtyping" (the substitutability contract OCP depends on). Worked discount-policy example, dispatch trace, diagram, pitfalls, and the OCP-vs-switch trade-off authored for this page.
🤖 Don't fully get this? Learn it with Claude
Stuck on Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles** (OO & Low-Level Design) and want to truly understand it. Explain Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Final Thoughts on the OpenClosed Principle and Its Relation to Other SOLID Principles** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.