Why this matters
The OCP, originally formulated by Bertrand Meyer, is at the heart of architectural thinking. Martin illustrates it with a financial reporting system that must migrate from a web view to a print view. If the system is structured so that responsibilities are separated and dependencies flow toward higher-level policy, the financial calculation component never needs to change — only a new rendering component is added.
The key insight is about protection: higher-level components must be protected from changes in lower-level components. When you add a new feature by adding a new class rather than modifying an existing one, you protect all existing tests and all existing behavior. The if/switch statement that grows with every new case is OCP's most common enemy — each new case is a modification to existing, tested, deployed code.
The problem
Every new output format requires editing the same function — changing tested, deployed code and risking regressions.
Bad
def generate_report(data: list, output_type: str) -> str:
if output_type == "web":
return "<html>" + render_html(data) + "</html>"
elif output_type == "pdf":
return render_pdf(data)
# Adding "excel" means touching existing, tested code — violation!
function generateReport(data: Row[], outputType: string): string {
if (outputType === "web") {
return "<html>" + renderHtml(data) + "</html>";
} else if (outputType === "pdf") {
return renderPdf(data);
}
throw new Error("Unknown output type");
// Adding "excel" means editing here — closed to extension!
}
The solution
An abstract renderer interface lets each format live in its own class. Adding Excel support means writing a new class — existing code is never touched.
Good
from abc import ABC, abstractmethod
class ReportRenderer(ABC):
@abstractmethod
def render(self, data: list) -> str: ...
class WebRenderer(ReportRenderer):
def render(self, data: list) -> str:
return "<html>" + render_html(data) + "</html>"
class PDFRenderer(ReportRenderer):
def render(self, data: list) -> str:
return render_pdf(data)
class ExcelRenderer(ReportRenderer): # added without touching any existing code
def render(self, data: list) -> str:
return render_excel(data)
def generate_report(data: list, renderer: ReportRenderer) -> str:
return renderer.render(data)
interface ReportRenderer {
render(data: Row[]): string;
}
class WebRenderer implements ReportRenderer {
render(data: Row[]): string {
return "<html>" + renderHtml(data) + "</html>";
}
}
class PDFRenderer implements ReportRenderer {
render(data: Row[]): string { return renderPdf(data); }
}
class ExcelRenderer implements ReportRenderer { // new class, zero edits elsewhere
render(data: Row[]): string { return renderExcel(data); }
}
function generateReport(data: Row[], renderer: ReportRenderer): string {
return renderer.render(data);
}
Key takeaway
Every new case added to an if/switch is a modification of existing code — a regression risk. The OCP says: design so that new behavior arrives as new code, never as edits to code that already works.