OCP: The Open/Closed Principle
Software artifacts should be open for extension but closed for modification — adding new behavior should add code, not change existing code.
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.
🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.
Hint: Every time you add a new case to an if/switch, you're violating OCP. A new class is safer.
✗ Your version