Loading learning content...
When developers first encounter the Open/Closed Principle, a natural question arises: How can something be both open and closed at the same time?
This seems like a logical impossibility. A door is either open or closed, not both. A store is either accepting customers or shuttered. How can software defy this basic logic?
The apparent contradiction isn't a flaw in the principle—it's a feature. Understanding why these requirements seem contradictory, and how they're actually compatible, reveals deep insights about software design. This page explores the paradox and sets the stage for its elegant resolution.
By the end of this page, you will understand why open and closed seem contradictory, recognize the different perspectives that create this tension, and prepare for the resolution through abstraction.
Let's state the apparent contradiction explicitly:
The "Open" Requirement: Software should accommodate new behaviors and requirements. It should be extensible, adaptable, capable of evolution.
The "Closed" Requirement: Software should not be changed. Its source code should remain stable, its behavior predictable, its tests valid.
The Apparent Conflict: If we can't change the code, how can we add new behavior? Doesn't adding new behavior require writing new code? And if we're writing to a file, isn't that changing it?
This confusion is understandable. It comes from conflating several distinct concepts:
| Concept A | Concept B | Why They're Different |
|---|---|---|
| Adding code | Modifying code | Adding a new file ≠ changing an existing file |
| Behavior extension | Behavior modification | Enabling new capabilities ≠ changing existing ones |
| The system | A component | System evolves ≠ every component changes |
| What code does | How code is structured | Functionality ≠ implementation |
The Key Insight: Scope of 'Open' vs. 'Closed'
The resolution begins with recognizing that "open" and "closed" apply to different aspects of the same entity:
A module can be closed (its code doesn't change) while the system containing it remains open (new modules can be added). The module's code is frozen; the system's capabilities grow.
Think of a filing cabinet. Each drawer can be 'closed' (sealed, contents fixed) while the cabinet itself remains 'open' (you can add more drawers). The existing drawers don't change when you add new ones. OCP operates on this principle—individual components are closed, but their container is open.
Understanding the paradox requires understanding its historical context. When Bertrand Meyer formulated OCP in 1988, software development looked very different:
The 1988 Reality:
In this environment, any modification to existing code was costly:
Meyer's insight was pragmatic: if we could design systems where new features required only new code, we could avoid the cascade of recompilation, relinking, and retesting.
123456789101112131415161718
# 1988: Adding a new shape to a graphics system ## Without OCP (Modification-Based)1. Modify graphics_base.h → All dependent files recompile2. Modify shape.c → Recompile shape module3. Modify renderer.c → Recompile renderer module4. Modify test_graphics.c → Recompile tests5. Full link of system → 45 minutes6. Manual regression testing → 2 hours Total impact: 3+ hours, high risk ## With OCP (Extension-Based)1. Create new file: circle.c → Compile only this file2. Register circle in config → No recompilation3. Test circle only → 10 minutes Total impact: 15 minutes, isolated riskThe Principle Endures
While compilation costs have decreased dramatically, Meyer's principle remains relevant for different reasons today:
| 1988 Motivation | 2024 Motivation |
|---|---|
| Compilation time | Reduced cognitive load |
| Linking complexity | Isolated testing |
| Retest burden | Safer deployments |
| Binary distribution | Microservice evolution |
| Physical media updates | Continuous delivery |
The mechanism of the benefit has changed, but the principle remains sound: changes isolated to new code are safer than changes to existing code.
OCP didn't emerge in a vacuum. It built on earlier ideas: David Parnas's work on information hiding (1972), the concept of abstract data types, and the emerging field of object-oriented programming. Understanding this lineage helps appreciate OCP's depth.
Beyond the logical paradox, there's a practical tension that makes OCP challenging. Real-world requirements don't always fit neatly into "extension" categories:
These scenarios reveal an important truth: OCP is an ideal, not an absolute law. Some changes genuinely require modifying existing code. The goal isn't to achieve perfect closure (impossible) but to:
The 80/20 Observation
In well-designed systems, roughly:
Poorly designed systems flip this ratio:
OCP is about engineering your way toward the good ratio.
Developers sometimes reject OCP because 'some modifications are inevitable.' This misses the point. OCP guides design toward extension over modification, not toward eliminating all modification. A system where 90% of changes are extensions is far better than one where 90% of changes are modifications.
The open/closed paradox operates at multiple levels, and understanding each level clarifies the resolution:
Level 1: The Source Code Level
At this level, the paradox is most acute. A file is either edited or not. Bytes change or they don't.
Resolution: OCP at the source level means new files, not edited files. The system expands through addition, not modification.
123456789101112
Initial State:├── PaymentProcessor.java (100 lines, tested)├── CreditCardHandler.java (50 lines, tested)└── PayPalHandler.java (45 lines, tested) After adding Apple Pay support (OCP-compliant):├── PaymentProcessor.java (100 lines, UNCHANGED)├── CreditCardHandler.java (50 lines, UNCHANGED)├── PayPalHandler.java (45 lines, UNCHANGED)└── ApplePayHandler.java (55 lines, NEW) The existing files are closed. The system is open via new files.Level 2: The Behavioral Level
At this level, we consider what the system does.
Resolution: Existing behaviors are closed (CreditCard processing works identically). New behaviors are open (ApplePay now works too). The system's behavioral repertoire expands without altering existing behaviors.
Level 3: The Contract Level
At this level, we consider interfaces and APIs.
Resolution: Contracts are highly closed (the PaymentHandler interface doesn't change). But contracts define extension points (any new class can implement PaymentHandler). Closed contracts enable open implementation.
Level 4: The Architectural Level
At this level, we consider system structure.
Resolution: Architecture defines where openness lives. A payment system is "open at the handler boundary" and "closed at the processor core." Architecture maps openness to the points most likely to need extension.
When evaluating OCP compliance, specify the level. 'Is this open?' is ambiguous. 'Is this open for new payment handlers at the behavioral level?' is precise and answerable.
Much confusion around OCP stems from treating "open" and "closed" as mutually exclusive states of a single property. In reality, they describe different properties entirely:
The Multi-Dimensional View
Think of openness and closure as independent axes:
HIGH OPENNESS
↑
|
Fragile | Ideal
(extensible but | (extensible AND stable)
unstable) |
|
LOW CLOSURE ←------------+------------→ HIGH CLOSURE
|
Rigid | Encapsulated
(neither extensible | (stable but not extensible)
nor stable) |
|
↓
LOW OPENNESS
The goal is the upper-right quadrant: high openness (extensible) AND high closure (stable).
This isn't a paradox—it's a design challenge. And the solution lies in abstraction.
The best designs maximize BOTH openness and closure. This seems impossible only if you think they're inversely related. They're not. A well-designed abstraction can be maximally open (infinite implementations possible) AND maximally closed (interface never changes).
Before diving into the full resolution (next page), let's preview how abstraction resolves the paradox:
The Core Mechanism
Abstraction creates a separation between:
The interface defines a stable contract. Implementations vary. Clients depend on the stable interface. New implementations extend capabilities without changing anything clients depend on.
12345678910111213141516171819202122232425
// CLOSED: This interface never changespublic interface Encoder { byte[] encode(String data);} // OPEN: New implementations can be added indefinitelypublic class Base64Encoder implements Encoder { /* ... */ }public class HexEncoder implements Encoder { /* ... */ }public class UrlEncoder implements Encoder { /* ... */ }// Future: Brotli, custom protocols, whatever // Client code: depends on the closed interfacepublic class DataProcessor { private final Encoder encoder; // Can be any implementation public void process(String data) { byte[] encoded = encoder.encode(data); // Same call, different behavior // ... }} // The client is CLOSED (doesn't change for new encoders)// The system is OPEN (new encoders can be added)// The interface is CLOSED (contract is stable)// The implementations are OPEN (unlimited variety)The Abstraction Inversion
The key insight is that abstraction inverts the relationship between stability and flexibility:
Without abstraction:
With abstraction:
This inversion is why abstraction resolves the open/closed paradox. The next page explores this mechanism in full depth.
Abstraction is not just a programming technique—it's the fundamental mechanism that makes OCP possible. By separating 'what' from 'how,' abstraction creates stable interfaces that enable unlimited implementation variation.
We've dissected the apparent contradiction at OCP's heart. Let's consolidate:
Coming Next:
We've set up the puzzle. Now it's time to solve it. The next page provides a comprehensive treatment of how abstraction—through interfaces, abstract classes, and polymorphism—resolves the open/closed paradox and enables the design of truly extensible, stable systems.
You now understand why the Open/Closed Principle appears paradoxical and why that appearance is misleading. The paradox dissolves once we recognize that open and closed describe independent dimensions of software design. Next, we'll see how abstraction provides the mechanism to achieve both.