Skip to main content
Clean Architecture 70 XP · 7 min

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.

Showing
Ad (728×90)

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.

Done with this lesson?

Mark it complete to earn XP and track your progress.