2.2.5 · D5Design Principles
Question bank — SOLID — Open - Closed Principle

Here are the two shapes of code that picture describes. Read them side by side — this is the contrast the whole question bank tests.

True or false — justify
Every use of an if/else statement violates OCP.
False. A single branch, or logic that has never had to grow, is fine — OCP targets the type/variation switch that keeps forcing edits, not all conditionals.
A class that has never been modified since it was written is automatically OCP-compliant.
False. "Not yet modified" may just mean "not yet challenged". OCP is about whether new behaviour can be added without editing it, not about its edit history.
Following OCP means you should never edit existing source code again.
False. Bug fixes, security patches, and performance tweaks all require editing code. OCP only says new features/variations shouldn't force edits to well-factored modules.
Adding a new elif branch and adding a new subclass both "extend" the system, so both satisfy OCP.
False. The
elif modifies an existing tested method (violation, the left side of the figure); the new subclass adds code and leaves old code frozen (compliant, the right side). Extension must be additive.OCP is impossible to achieve without inheritance.
False. Polymorphism (the enabler) can come from interfaces, first-class functions/delegates, mixins, or duck typing — inheritance is just one path, and composition is often the better one. See Polymorphism.
If checkout takes a Discount abstraction, adding BlackFriday(Discount) keeps checkout closed for modification.
True.
checkout only depends on the apply contract, so a new implementation never touches its source — this is the Strategy Pattern realisation of OCP.Adding a new method to an existing interface is "extension", so it respects OCP.
False. It modifies the contract and forces every implementor to change (in pre-default-method Java it won't even compile). True extension adds a new implementor behind an unchanged interface.
Making every single class implement an interface "just in case" is good OCP hygiene.
False. That is speculative generality — one-implementation interfaces add indirection with no proven variation axis. OCP asks you to abstract where change has actually happened, not everywhere.
OCP and DIP (Dependency Inversion Principle) are the same principle.
False. DIP is the structural rule "depend on abstractions, not concretions"; OCP is the goal (extension without modification) that DIP's structure enables. Related, not identical — see SOLID — Dependency Inversion Principle.
A Shape base class whose area() you must edit whenever a subclass is added still respects OCP.
False. If the base's
area() contains a type-switch, you re-open it per shape — the switch just moved. Each subclass must own its own formula for the base to stay closed.In a compiled library, adding a brand-new subclass can never break existing callers.
Mostly true for the new class itself, but if you also add or reorder methods/fields in the shared base or interface, you can break binary compatibility — old compiled callers may fail to link against the new build even without source changes.
Spot the error
"My function has a 6-branch switch on payment_type; I'll add branch 7 for crypto — that's extending behaviour, so it's OCP."
The error is calling an edit an extension. Adding branch 7 re-opens and re-tests all 6 existing cases (the left side of figure s01). Real OCP would make each payment a class behind a
Payment interface so crypto is a new file, old code untouched."I made an AbstractLogger interface with exactly one implementation, FileLogger, so my logging is OCP-ready."
Over-engineering / premature abstraction. With one implementation and no history of variation, the interface is pure indirection. Add the abstraction only when a second logging axis actually appears (Rule of Three — the Fowler refactoring one).
"To follow OCP I edited the Shape base class to add a triangle_area() helper that subclasses call."
You modified the frozen contract. The base grew a triangle-specific method — the very code you wanted closed. Triangle's area belongs inside
Triangle.area(), leaving Shape untouched."I abstracted on the very first requirement, before any second case existed, to be safe."
Violates the Rule of Three from the other side. Abstracting with zero evidence of variation guesses the wrong axis and adds cost for behaviour that may never come. Tolerate the first (and second) case, refactor on the third.
"AreaCalculator.total iterates and does if isinstance(s, Circle): ... to pick the formula."
The type-switch has leaked back into the calculator. It is now open for modification again — every new shape edits
total. It must call s.area() and trust the abstraction, never inspect concrete types."I shipped a public interface Payment, and to support refunds I just added a refund() method to it in version 2."
You broke every external implementor and (in compiled languages) binary compatibility. That is modification of a shipped contract, not extension. Instead add a
default method, an optional sub-interface Refundable, or a new Payment2 — leaving v1 implementors compiling."To make my Report class support timestamps I edited BaseReport to add stamping logic inline."
You re-opened the base everyone shares. Compose the behaviour instead: pull stamping in as a
TimestampMixin (or inject a formatter) so BaseReport stays closed and the new capability is added sideways.Why questions
Why does editing already-tested code carry a hidden cost that adding a new class avoids?
Editing forces re-review, re-test, and re-deploy of the whole file and risks regressions in unrelated cases sharing that method; a new class leaves the proven code frozen, so its guarantees still hold (this is exactly what figure s02 plots over time).
Why does adding a method to a widely-implemented interface hurt more than adding a subclass?
A new subclass is isolated new code; a new interface method is a contract change that fans out to every implementor, each of which must now be edited (and, in pre-default-method Java, won't compile until it is). The blast radius is the whole implementor set, not one file.
Why is polymorphism, not conditionals, the mechanism OCP prefers for variation?
A conditional centralises every case in one method that must reopen per case; polymorphism distributes each case into its own class or function, so a new case is a new unit — additive, not invasive. The mechanical move is Refactoring — Replace Conditional with Polymorphism: you take each
if-branch body and lift it into an overriding method (or a strategy function), then delete the switch.Why is composition often preferred over inheritance for OCP?
Inheritance couples a subclass to the whole parent (fragile base class, and interface-method-add breakage); composition plugs in a small, focused behaviour (a function, delegate, or mixin) with a tinier contract, so extension points stay independent and swappable at runtime.
Why does OCP lean on SRP (Single Responsibility) to work well?
SRP ensures a module has one reason to change, so the "varying axis" is clean and isolated; you can then wrap exactly that axis in an abstraction. A class doing five things has five tangled axes and no clean seam to close. See SOLID — Single Responsibility Principle.
Why can over-applying OCP make code harder to maintain?
Every speculative interface adds a layer of indirection a reader must trace through; with no real second implementation, that indirection buys nothing and obscures the one concrete path that actually runs.
Why does the "Rule of Three" pick three rather than two?
Two points could be coincidence — one exception, not a pattern. The third variation is strong evidence the axis is genuinely open, justifying the upfront abstraction cost. Before that, the concrete branch is cheaper and clearer.
Why should a public API sometimes be abstracted earlier than the Rule of Three suggests?
Because reversing a public design is expensive — external callers depend on it. When the cost of a wrong guess is high and change is hard to undo, you trade a little speculation for future flexibility, unlike a cheap-to-refactor internal detail.
Why does OCP explicitly not forbid bug fixes?
A bug means existing code is wrong, not that a new variation arrived; fixing it restores intended behaviour. OCP governs how you grow behaviour, so patching correctness is outside its scope.
Why is depending on the interface (not the concrete type) the crux of "closed for modification"?
The high-level code compiles/runs against a fixed contract, so new implementations behind that contract are invisible to it — nothing in its source needs to change when they appear.
Why is the Template Method Pattern also an OCP realisation, like Strategy Pattern?
Template Method freezes the algorithm skeleton in a base method and leaves overridable steps as the extension points — new behaviour comes from overriding steps (new subclass), never from editing the skeleton.
Edge cases
What is the OCP status of a brand-new module with only one implementation and no interface yet?
Perfectly fine — it is not a violation. With zero observed variation, an interface would be premature. OCP compliance is judged when a second variation actually arrives.
How does OCP look in a purely functional codebase with no classes at all?
Identical goal, different tool: the extension point is a higher-order function. Instead of a
Discount subclass, you pass a discount function price -> price into checkout; a new rule is a new function value, and checkout (which just calls whatever function it was handed) is never edited. Pattern matching on a sum type is the functional equivalent of the if/elif violation — adding a case reopens every match.In a functional style, what plays the role of the "frozen contract" that classes get from an interface?
The function type signature (e.g.
Float -> Float). Any function of that shape is a valid plug-in, exactly like any subclass satisfying an interface. The signature is the abstraction; the concrete functions are the extensions.What is "binary compatibility" and why does it matter for the "closed" part of OCP in compiled languages?
Binary compatibility means already-compiled callers keep linking against a new build without recompiling. Adding a new subclass is usually safe, but adding/removing/reordering methods or fields in a public base class or interface can break the link — so in large systems the "closed" guarantee must be defended at the API boundary, not just the source level.
If a requirement demands changing the contract itself (e.g. area() must now take a unit argument), does OCP save you?
No — a contract change ripples to every implementation and every caller by definition. OCP protects against new implementations, not against redesigning the abstraction; that is a genuine breaking change. Mitigate with a new overload/interface rather than mutating the shipped one.
What if the "variation" is a one-off that will provably never recur (a legal hardcode for a single client)?
A plain conditional is acceptable; abstracting a never-repeating case is speculative generality. OCP is about predicted variation, so no-recurrence means no abstraction warranted.
A degenerate NoDiscount that returns the price unchanged — is that a wasteful class?
No, it is a valuable null/identity implementation. It lets
checkout always call apply without a None check, keeping the high-level code branch-free and closed — a deliberate OCP pattern.What happens to "closed for modification" if two variation axes appear at once (e.g. shapes and rendering styles)?
A single interface can't cleanly close both; you risk a combinatorial
if/if. Separate each axis into its own abstraction (or combine via composition/visitor) so each stays independently extensible — composition shines here.Is a giant if/elif with only two branches already an OCP smell requiring refactor?
Not yet. Two branches are within tolerance (Rule of Three). Refactor on the third variation; refactoring at two often abstracts the wrong axis and adds cost prematurely.
If adding a feature requires editing exactly one line in one existing config/registry, is OCP violated?
It's a grey zone. A pure plugin registry that only registers a new class (no logic edits) is usually acceptable; if that line contains behavioural
if-logic, it's a violation in disguise. Prefer auto-discovery to keep it truly closed.Recall One-line self-test
Ask of any change: "Am I adding a new file, or reopening a proven one?" Adding ::: on the OCP path. Reopening working logic ::: a violation (unless it's a genuine bug fix or contract redesign).