How the Open/Closed Principle Enhances Software Development
The Fragile Base Class Problem
Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
π― System Scenario & Requirements
System Goal
Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
- You risk: - Breaking credit card handling.
- - Merging conflicts from other teams editing the same file.
Constraints & Assumptions
- That's only worth the cost when variation is genuinely expected.
- The trap: abstracting too early.
β‘ Execution Stages & Decision Trace
Stage 1 β Open for Extension, Closed for Modification" β What That Actually Means
Every time you open a file to add a new feature, you risk breaking something that already works.
OCP says: instead of modifying existing code, design it so you can add new behavior by adding new code β not changing old code.
Deep Dive
Scenario context
Here's a payment processor that violates OCP:
class PaymentProcessor:
def process(self, payment_type, amount):
if payment_type == "credit_card":
print(f"Processing credit card: ${amount}")
elif payment_type == "paypal":
print(f"Processing PayPal: ${amount}")
# Every new payment method = edit THIS file
Each time you add a new payment method (Apple Paβ¦
This walkthrough focuses on The Fragile Base Class Problem.
Before making a design choice, clarify:
- Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
- You risk: - Breaking credit card handling.
- Merging conflicts from other teams editing the same file.
Concepts in play: design-principles-solid, jvm-internals-and-garbage-collection, low-level-design-systems, python-concurrency-nogil
Stage 1 β Open for Extension, Closed for Modification" β What That Actually Means
- Every time you open a file to add a new feature, you risk breaking something that already works.
- OCP says: instead of modifying existing code, design it so you can add new behavior by adding new code β not changing old code.
- The classic metaphor: a plugin system .
- You don't modify Firefox to add an ad-blocker.
Observable outcome: You don't modify Firefox to add an ad-blocker.
Stage 2 β OCP via Polymorphism: Extending Without Modifying
- The fix: define a common abstraction and extend it.
- PaymentProcessor is now closed for modification.
- You can add 50 new payment methods and never touch it.
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount: float) -> None:
...
class CreditCard(PaymentMethod):
def process(self, amount):
print(f"Processing credit card: ${amount}")
class PayPal(PaymentMethod):
def process(self, amount):
print(f"Processing PayPal: ${amount}")
class CryptoWallet(PaymentMethod): # NEW β no existing file touched
def process(self, amount):
print(f"Processing crypto: ${amount}")
class PaymentProcessor:
def process(self, method: PaymentMethod, amount: float):
method.process(amount) # Never changes
classDiagram
class PaymentMethod {
<>
+process(amount)
}
PaymentMethod <|-- CreditCard
PaymentMethod <|-- PayPal
PaymentMethod <|-- CryptoWallet
PaymentProcessor --> PaymentMethod
Observable outcome: You can add 50 new payment methods and never touch it.
Stage 3 β Deep Dive: Java Interface Implementation
- In Java, the same pattern uses interfaces: Testing benefit: You can write a FakePayment implementing PaymentMethod for unit tests without any production code changes.
- The Java version also demonstrates an important architectural boundary: the PaymentProcessor class lives in a separate module and has zero compile-time dependency on CreditCard , CryptoWallet , or any concrete payment tβ¦
- Those classes are loaded at runtime, which means you can add, remove, or swap payment providers via dependency injection without recompiling PaymentProcessor .
- This is OCP operating at the module boundary level β a pattern you'll find in every major Java framework from Spring to Jakarta EE.
public interface PaymentMethod {
void process(double amount);
}
public class CreditCard implements PaymentMethod {
public void process(double amount) {
System.out.printf("Credit card: $%.2f%n", amount);
}
}
public class CryptoWallet implements PaymentMethod { // New β zero changes to CreditCard
public void process(double amount) {
System.out.printf("Crypto: $%.2f%n", amount);
}
}
public class PaymentProcessor {
public void charge(PaymentMethod method, double amount) {
method.process(amount); // Closed β never modified
}
}
classDiagram
class Shape {
<>
+area() double
+perimeter() double
}
class Circle {
-double radius
+area() double
+perimeter() double
}
class Rectangle {
-double width
-double height
+area() double
+perimeter() double
}
class Triangle {
-double base
-double height
+area() double
+perimeter() double
}
class AreaCalculator {
+totalArea(shapes) double
}
Shape <|.. Circle
Shape <|.. Rectangle
Shape <|.. Triangle
AreaCalculator ..> Shape
Observable outcome: This is OCP operating at the module boundary level β a pattern you'll find in every major Java framework from Spring to Jakarta EE.
Stage 4 β OCP Extension Flow
- The key insight of OCP is that adding new behavior never requires touching PaymentProcessor .
- New payment types are added by creating new classes that plug into the existing abstraction.
- When Apple Pay is needed, a developer creates ApplePayMethod implements PaymentMethod and passes it to PaymentProcessor .
- Zero lines of PaymentProcessor or any existing payment class are changed β that is OCP in action.
flowchart LR
A[PaymentProcessor.process] --> B[PaymentMethod interface]
B --> C[CreditCard]
B --> D[PayPal]
B --> E[CryptoWallet]
E --> F[NEW: no existing code modified]
G[Add Apple Pay] --> H[Create new class]
H --> B
Observable outcome: Zero lines of PaymentProcessor or any existing payment class are changed β that is OCP in action.
Stage 5 β Real-World Application: OCP in Production Systems
- OCP is not just a class design pattern β it appears throughout distributed systems and product architecture.
- Real-world example β VS Code extensions: VS Code defines extension APIs (the abstraction).
- Extension authors add new functionality (language support, themes, debuggers) by implementing those APIs.
- The VS Code core is never modified to support new extensions.
Observable outcome: The VS Code core is never modified to support new extensions.
Stage 6 β Hands-On: Apply OCP to a Report Generator
- This exercise demonstrates the most common OCP violation pattern: a class that grows a new elif branch for every new output format, using ReportGenerator as the example because report formats are a natural extension poiβ¦
- This particular violation was chosen because it mirrors real-world code you will find in ETL pipelines, data export tools, and document services.
- As you read through the violation code, focus on the line you would have to change to add Excel support β that modification point is the exact boundary OCP asks you to protect.
- OCP-compliant refactor: Before vs After: π Strategy Pattern: OCP Applied to Sorting Algorithms This class diagram shows the Strategy pattern applied to sorting: Sorter holds a reference to the SortStrategy interface anβ¦
# π OCP violation β every new format requires modifying this class
class ReportGenerator:
def generate(self, data, format_type: str):
if format_type == "pdf":
return self._render_pdf(data)
elif format_type == "csv":
return self._render_csv(data)
elif format_type == "html":
return self._render_html(data)
# Adding Excel requires modifying this class
def _render_pdf(self, data): ...
def _render_csv(self, data): ...
def _render_html(self, data): ...
from abc import ABC, abstractmethod
class ReportFormatter(ABC):
@abstractmethod
def format(self, data) -> str: ...
class PDFFormatter(ReportFormatter):
def format(self, data) -> str:
return f"[PDF] {data}"
class CSVFormatter(ReportFormatter):
def format(self, data) -> str:
return ",".join(str(v) for v in data)
class ExcelFormatter(ReportFormatter): # NEW β zero existing code changed
def format(self, data) -> str:
return f"[Excel] {data}"
class ReportGenerator:
def generate(self, data, formatter: ReportFormatter) -> str:
return formatter.format(data) # Never changes
Observable outcome: OCP-compliant refactor: Before vs After: π Strategy Pattern: OCP Applied to Sorting Algorithms This class diagram shows the Strategy pattern applied to sorting: Sorter holds a reference to the SortStrategy interface anβ¦
End-to-end outcome
After working through the stages, you should be able to explain:
- Apply OCP reactively, not preemptively.
- The first time you write payment processing, write CreditCard directly.
Alternatives considered
Decision Guide
Decision Guide
Failure and degraded modes
Trade-offs & Failure Modes: When OCP Is Over-Engineering
- Detection: That's only worth the cost when variation is genuinely expected.
- User or operator impact: That's only worth the cost when variation is genuinely expected.
