Chapter 8: OCP โ The Open-Closed Principle
"A software artifact should be open for extension but closed for modification."
โ Bertrand Meyer (1988), restated by Martin
๐ For New Learnersโ
What OCP Saysโ
If you want to add a new feature, you should be able to add new code rather than change existing code.
Why? Because every time you change existing code, you risk breaking everything that already works. If you can add features by only adding code โ new classes, new modules โ you can never break existing behavior.
A Thought Experimentโ
Imagine a financial reporting system that displays data on a web page. A new requirement: also generate a printed report.
OCP violation (modification):
public class ReportGenerator {
public void generate(Report data, OutputType type) {
if (type == WEB) { /* web-specific logic */ }
else if (type == PRINT) { /* print-specific logic */ }
// Adding PDF means editing this class again
}
}
Every new output format requires modifying ReportGenerator. Every modification risks breaking web and print.
OCP compliance (extension):
public interface ReportPresenter {
void present(Report data);
}
public class WebReportPresenter implements ReportPresenter { ... }
public class PrintReportPresenter implements ReportPresenter { ... }
public class PdfReportPresenter implements ReportPresenter { ... } // new, no changes elsewhere
Adding PDF adds a new class. Existing classes are untouched. They are closed for modification.
The Direction of Protectionโ
OCP is achieved by controlling which component knows about which:
- High-level components (business logic, policy) should be protected from changes in low-level components (presenters, formatters, I/O)
- This means: high-level components must NOT depend on low-level ones
- Instead: both depend on abstractions (interfaces)
This is why OCP and the Dependency Inversion Principle (Chapter 11) are deeply connected.
๐ฌ Senior Deep Diveโ
OCP as the Goal of Architectureโ
Martin makes a bold statement: OCP is the driving motivation behind Clean Architecture itself. The entire model of concentric rings, the dependency rule, the boundary-drawing โ all of it exists to make high-level policy closed to changes in low-level details.
The "closed" part is about protection hierarchy:
Business Rules (closed to everything below)
โ depends on abstractions
Use Cases (closed to presenters and DB)
โ depends on abstractions
Interface Adapters (closed to frameworks)
โ depends on abstractions
Frameworks & Drivers (open, changes freely)
When a framework changes (Spring Boot version upgrade, JPA API change), only the outermost ring is affected. Business rules are not recompiled, not retested.
Directional Control and Information Hidingโ
Two mechanisms achieve OCP in practice:
Directional Control: Interfaces are placed at boundaries so that dependencies point the right direction. A ReportPresenter interface lives in the use case layer. Both the use case and the web presenter depend on it โ the dependency arrow points toward the use case, not toward the presenter.
Information Hiding: A component that should be closed doesn't expose its internals. The FinancialReportController in the web adapter doesn't need to know which database stores the financial data โ that knowledge is hidden behind a FinancialDataGateway interface.
Spring Implementationโ
// Use Case โ closed to presentation concerns
public class GenerateFinancialReportUseCase {
private final FinancialDataGateway gateway; // interface
private final FinancialReportPresenter presenter; // interface
public void execute(ReportRequest request) {
FinancialData data = gateway.fetchFor(request.period());
presenter.present(FinancialReport.from(data));
}
}
// New output format โ ZERO changes to use case
@Component
public class PdfFinancialReportPresenter implements FinancialReportPresenter {
public void present(FinancialReport report) {
// PDF generation logic
}
}
New requirement โ new class. Existing use case: untouched, unconcerned.
OCP and Testsโ
OCP also applies to test doubles. Because production code depends on interfaces, tests substitute implementations freely:
class GenerateFinancialReportUseCaseTest {
@Test void generatesReport() {
var fakeGateway = new InMemoryFinancialGateway();
var capturePresenter = new CapturingPresenter();
var useCase = new GenerateFinancialReportUseCase(fakeGateway, capturePresenter);
useCase.execute(new ReportRequest(Q3_2024));
assertThat(capturePresenter.lastReport()).isNotNull();
}
}
No Spring context. No database. No HTTP. The use case is closed to all infrastructure โ and thus testable in microseconds.
Summaryโ
| Concept | Key Point |
|---|---|
| OCP definition | Add features by adding code, not by modifying existing code |
| Protection hierarchy | High-level policy is closed; low-level details are open |
| Mechanism | Interfaces at boundaries + dependency inversion |
| Architecture connection | OCP is the core motivation for Clean Architecture's concentric rings |
| Spring pattern | @Component implementing an interface defined in the inner layer |