What does the Open/Closed Principle (the "O" in SOLID) mean by "open for extension, closed for modification"?
answer
- Add new code, don't edit old code
- Abstraction = the hinge: closed core, open implementations
- Kill the switch-on-type; each branch becomes a class
- Closed only against the anticipated axis of change
- Meyer = subclass; Martin = interface + polymorphism
basics
~10 sYou should be able to add new behavior to a module by adding new code (a new class or plug-in), without editing and re-testing the existing, already-working code.
solid answer
~50 sThe Open/Closed Principle (OCP), the "O" in SOLID, says a software entity — class, module, function — should be **open for extension** (its behavior can be varied for new requirements) but **closed for modification** (its existing source, which is already written, reviewed, tested and shipped, does not have to change to do so). The practical mechanism is an abstraction: the stable code depends on an interface/abstract contract, and new behavior arrives as a new implementation of that contract that gets plugged in. The classic smell that OCP addresses is a growing `if type == A … else if type == B …` or `switch` on a type code, repeated in several places, where every new case forces edits in existing files. The payoff is smaller blast radius: fewer regressions, less re-testing, less merge conflict, independent deployability. The cost is an extra indirection, so it is applied where change is actually expected, not everywhere.
code
typescript · 14 lines// Closed for modification: this never changes when a format is added.
interface Renderer { readonly name: string; render(r: Report): Bytes }
class ReportService {
constructor(private readonly renderers: Map<string, Renderer>) {}
render(r: Report, format: string): Bytes {
const renderer = this.renderers.get(format)
if (!renderer) throw new Error(`no renderer for ${format}`)
return renderer.render(r) // polymorphic call = the extension point
}
}
// Open for extension: a new file, nothing existing edited.
class XlsxRenderer implements Renderer { name = 'xlsx'; render(r: Report) { /* ... */ } }go deeper
State the definition and give the switch-statement-to-interface example. Naming polymorphism as the mechanism is enough.
Show the refactor concretely (conditional → strategy/registry), and name decorator and plug-in registration as other routes. Mention that only anticipated axes of change are protected.
Contrast Meyer's inheritance formulation with Martin's polymorphic one, tie OCP to DIP, and be explicit about the cost: indirection, speculative generality, YAGNI. Say when you'd deliberately keep an exhaustive switch.
Frame OCP as a decision about where variation is expected, applied at module/service/API scale: stable published contracts, plug-in boundaries, backward-compatible schema evolution, and the organizational payoff of teams extending without coordinating edits.
## The words, defined - **Software entity**: any unit of code you can name and reuse — a function, class, module, package, service. - **Open for extension**: the entity's behavior can be made to do something new — handle a new payment type, a new export format, a new discount rule. - **Closed for modification**: to get that new behavior, you do **not** have to open and edit the entity's existing source code. Its file stays byte-identical; you add new files instead. - **Abstraction**: a contract (interface, abstract class, function type, protocol) that names *what* is done without saying *how*. Concrete implementations supply the *how*. - **Polymorphism**: the ability of a single call site (`shape.area()`) to run different code depending on which concrete implementation is behind the reference. This is the engine that makes OCP possible in object-oriented code. At first hearing this sounds contradictory — how can something change without changing? The resolution is that *behavior of the system* changes while *the source of the existing modules* does not. The variation point was designed in ahead of time as an abstraction, and new code slots into it. ## The canonical shape of the problem Imagine a reporting module: ``` function render(report, format): if format == "pdf": ...pdf code... else if format == "csv": ...csv code... else if format == "html": ...html code... ``` Add XLSX and you must edit `render`. Worse, in real systems the same `switch` on `format` appears in three other places (file extension, MIME type, size estimate), so one new format means four coordinated edits — and if you miss one, you get a runtime bug rather than a compile error. This is called **shotgun surgery** (one conceptual change scattered across many files). The OCP-conforming version introduces the abstraction: ``` interface Renderer { name(); mimeType(); render(report) } ``` `render` now looks up a `Renderer` in a registry/map and calls it. Adding XLSX = adding one new file implementing `Renderer` and registering it. Nothing existing is edited, so nothing existing can regress. ## Why "closed" is valuable, concretely 1. **Regression risk**: code you did not touch cannot break (modulo shared state). Tests you already have stay valid. 2. **Review and blast radius**: a reviewer reads one new file, not a diff smeared over ten. 3. **Merge conflicts**: two teams adding two formats touch disjoint files instead of both editing the same `switch`. 4. **Distribution**: if the closed module ships as a compiled library, binary, or separately deployed service, a consumer *cannot* edit it — extension via plug-in is the only option available at all. ## Two historical formulations (both are "OCP") - **Bertrand Meyer (1988)** stated it in terms of **inheritance**: once a class is published and used, you do not modify it; you create a *subclass* that adds or overrides behavior. The original class stays frozen for existing clients. - **Robert C. Martin (1990s, "polymorphic OCP")** restated it in terms of **abstract interfaces**: clients depend on a fixed abstract interface; multiple implementations vary behind it, and the implementation can be swapped without recompiling clients. This is the version practiced today, and it composes with the Dependency Inversion Principle (high-level policy depends on abstractions, not on details). ## How you actually achieve it - **Polymorphism / strategy**: replace conditional-on-type with a call through an interface; each branch becomes an implementation. - **Decorator / wrapper**: wrap an existing implementation to add cross-cutting behavior (caching, retry, logging, metrics) without editing the wrapped class. - **Plug-in / registry hooks**: the core declares an extension point and discovers implementations at startup (service loader, dependency-injection container collecting all beans of a type, a config-driven map). - **Callbacks / higher-order functions**: pass in the varying behavior as a function parameter — the lightweight, language-neutral form of Strategy. - **Data-driven configuration**: move the variable part into data (rule tables, templates) so "new behavior" is a new row, not new code. ## The essential caveat: you cannot be closed against everything No design is closed against *all* possible changes — only against the **axis of change you anticipated**. A renderer registry is closed against "new format" but wide open to modification if the requirement becomes "every render must be async and streamed": that changes the interface itself, and every implementation must change. Choosing which axis to protect is a judgment call informed by what has actually changed before. Guessing wrong produces **speculative generality**: layers of abstraction that serve exactly one implementation forever, costing readability and indirection while buying nothing. The mature reading of OCP is therefore: write the simple direct code first; when the *second* variant appears, refactor to the abstraction; do not build the plug-in framework for a variant nobody has asked for. ## Edge cases and honest limits - **Bug fixes are modifications, and that's fine.** OCP is about extending behavior for new requirements, not a ban on ever editing code. - **Adding a case to a closed set can be worse.** If a type genuinely has a fixed set of variants (e.g. `Success | Failure`), an exhaustive `switch` that the compiler checks may be *safer* than an open interface, because adding a variant then produces compile errors at every place that must be updated rather than silent wrong behavior. - **The expression problem**: interface-based OCP makes it cheap to add new *types* (implementations) and expensive to add new *operations* (a new method forces edits to every implementation). The `switch`-based style is the mirror image. You choose which dimension you expect to grow. - **Runtime openness costs something**: dynamic dispatch defeats some inlining, plug-in discovery complicates startup and static analysis, and "anyone can register anything" widens the security/trust surface.
- If OCP forbids modifying existing code, how do you ever fix a bug?You modify it. OCP targets *new behavior driven by new requirements*, not corrections. A bug means the existing code does not meet its already-agreed contract, so editing it is the right fix — and it does not add a new variation point.
- Does adding a new method to an existing interface violate OCP?Effectively yes, for that interface's implementers: every implementation must now change. That is the expression-problem trade-off — interface-based OCP is closed against new implementations but open to churn when new operations are added. Mitigations are default methods, a second smaller interface (ISP), or an abstract base class.
- How is OCP related to the Dependency Inversion Principle?DIP is the structural rule that makes OCP achievable: high-level policy and low-level details both depend on an abstraction owned by the policy side. Once the dependency points at an abstraction, a new detail can be plugged in without touching the policy — which is exactly OCP.
A wall socket. The house wiring is closed — you never re-wire the wall to use a new lamp. The socket is the fixed contract; any appliance that fits the plug extends what the house can do. But the socket is only open along the axis it anticipated: switch the country's voltage and every appliance breaks, because that change crosses the contract itself.