PushBackLog

Open/Closed Principle (OCP)

Advisory enforcement Complete by PushBackLog team
Topic: quality Topic: architecture Methodology: SOLID Skillset: any Technology: generic Stage: execution Stage: review

Open/Closed Principle (OCP)

Status: Complete
Category: SOLID Principles
Default enforcement: Advisory
Author: PushBackLog team


Tags

  • Topic: quality, architecture
  • Methodology: SOLID
  • Skillset: any
  • Technology: generic
  • Stage: execution, review

Summary

Software entities — classes, modules, functions — should be open for extension but closed for modification. When new behaviour is required, you add new code rather than changing existing code that already works. This protects existing functionality from regression while allowing the system to grow.


Rationale

The cost of modification

Every time you modify working code, you risk breaking something that currently works. Without comprehensive tests (and even with them), modifications to shared or central logic can have knock-on effects that are difficult to predict. The OCP is a design strategy that minimises the surface area for regression: if you can add a new payment method by adding a StripePaymentProvider class rather than by modifying PaymentProcessor, then PaymentProcessor — and everything that depends on it — cannot break.

Extension points as future-proofing

The OCP doesn’t mean never changing code; it means designing systems with deliberate extension points so that common change scenarios don’t require modifications to stable, tested code. A system designed with OCP in mind looks ahead: what is likely to vary? Those variation points become interfaces or plugin slots. What is unlikely to vary? That logic is encapsulated and sealed.

Robert C. Martin framed it this way: software changes in two directions — fixing bugs (modification is appropriate) and adding capabilities (extension is the goal). The OCP is primarily about capability extension.

The connection to abstraction

OCP is practically inseparable from DIP. “Open for extension” almost always means “depends on an abstraction that can have new implementations added”. If PaymentProcessor depends on IPaymentProvider rather than on StripeClient directly, new payment providers can be added — opening the system — without touching PaymentProcessor — closing it to modification.


Guidance

Identify variation axes first

Before designing extension points, identify what is actually likely to change. Over-engineering OCP into stable code creates pointless abstractions. Ask:

  • What types of this thing could we need in the future?
  • What parts of this logic are policy (unlikely to change) versus mechanism (likely to vary)?
  • What has changed in this area in the last 6 months?

Design extension points for things that demonstrably vary. Leave things closed-but-modifiable until a second case exists that justifies the abstraction.

Common implementation patterns

PatternWhen to use
StrategyVary an algorithm or behaviour — pass different implementations of an interface
DecoratorAdd behaviour around an existing implementation without changing it
Plugin / ProviderRegister new implementations at a higher level; core code never knows about specifics
Template MethodFix the skeleton of an algorithm in a base class; let subclasses fill in the steps

The rule of three

Applying OCP prematurely produces speculative abstractions that almost never match what you actually need. A pragmatic approach: the first case is implemented concretely. When a second case arrives, the tension between the two reveals what actually needs to vary. On the third case, you have enough evidence to design an appropriate abstraction. This is often called the “rule of three”.


Examples

Before OCP (brittle)

class ReportGenerator {
  generate(type: string, data: unknown) {
    if (type === 'pdf') {
      // PDF-specific logic
    } else if (type === 'csv') {
      // CSV-specific logic
    } else if (type === 'excel') {
      // Excel-specific logic — requires modifying this class
    }
  }
}

Every new report format requires opening ReportGenerator and modifying it. Tests for PDF and CSV rendering are at risk each time.

After OCP (extensible)

interface ReportFormat {
  render(data: unknown): Buffer;
}

class PdfFormat implements ReportFormat { ... }
class CsvFormat implements ReportFormat { ... }
class ExcelFormat implements ReportFormat { ... }  // New format — no change to generator

class ReportGenerator {
  generate(format: ReportFormat, data: unknown): Buffer {
    return format.render(data);
  }
}

ReportGenerator never changes. Adding a new format is adding a new class.


Anti-patterns

1. Hardcoded dispatch on type strings or enums

if (type === 'x') ... else if (type === 'y') is the canonical OCP violation. Every new case requires a modification to the dispatch site, not an addition alongside it.

2. Premature OCP / speculative abstractions

Writing an interface with one implementation “just in case” creates complexity without benefit. Abstractions that don’t serve a current second use case are a maintenance cost, not an investment. Defer OCP refactoring until a real second case arrives.

3. Treating bug fixes as extensions

OCP does not mean never modify code. Fixing a bug in existing logic is modification — and it’s correct. OCP governs capability extension, not correction.

4. Extension points too granular

Making every method overridable “for flexibility” produces a system that is theoretically open but practically incomprehensible. Extension points should be coarse-grained and deliberate, not omnipresent.



Part of the PushBackLog Best Practices Library. Suggest improvements →