How the Open/Closed Principle Enhances Software Development
Software entities should be open for extension, but closed for modification. We explain the 'O' i...
Abstract AlgorithmsTLDR: The Open/Closed Principle (OCP) states software entities should be open for extension (add new behavior) but closed for modification (don't touch existing, tested code). This prevents new features from introducing bugs in old features.
๐ "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. You write a new plugin that extends Firefox. The browser itself never changed.
๐ The Fragile Base Class Problem
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 Pay, Crypto), you edit PaymentProcessor.process(). You risk:
- Breaking credit card handling.
- Merging conflicts from other teams editing the same file.
- Growing the
if/elifchain indefinitely.
โ๏ธ OCP via Polymorphism: Extending Without Modifying
The fix: define a common abstraction and extend 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
PaymentProcessor is now closed for modification. You can add 50 new payment methods and never touch it.
๐ง Java Interface Implementation
In Java, the same pattern uses interfaces:
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
}
}
Testing benefit: You can write a FakePayment implementing PaymentMethod for unit tests without any production code changes.
โ๏ธ When OCP Is Over-Engineering
OCP adds indirection. That's only worth the cost when variation is genuinely expected.
| Scenario | Apply OCP | Skip OCP |
| Multiple implementations of the same behavior (e.g., payment types) | โ | โ |
| Behavior that varies by external configuration or runtime | โ | โ |
| Single implementation that will never vary | โ | โ |
| Early-stage prototyping where requirements are unclear | โ | โ |
| One-off scripts or CLI tools | โ | โ |
The trap: abstracting too early. If you build an interface for a class that only ever has one implementation, you've added complexity with no benefit. OCP is reactive โ apply it when the second variant appears, or when you are confident variation is coming.
๐ Summary
- Closed for modification: Once a class is tested and deployed, resist changes that could introduce regressions.
- Open for extension: Use abstract classes or interfaces to define the contract; add new types as new classes.
- Polymorphism is the mechanism: A
PaymentProcessorthat takes anyPaymentMethodnever needs to know about new payment types. - Don't over-abstract: OCP pays off when variation is real. One class with one job needs no abstraction layer.
๐ Practice Quiz
A developer adds a new
eliffor every new report format to aReportGeneratorclass. Which principle is being violated?- A) Single Responsibility Principle.
- B) Open/Closed Principle โ the class requires modification for extension.
- C) Liskov Substitution Principle.
Answer: B
What is the primary mechanism for achieving OCP in object-oriented code?
- A) Static methods.
- B) Polymorphism via abstract classes or interfaces.
- C) Inheritance without abstraction.
Answer: B
You have a class that formats invoices. Currently it only formats PDFs and never will do anything else. Should you apply OCP?
- A) Yes, always add an abstraction layer.
- B) No โ single implementation, no anticipated variation. OCP adds cost without benefit.
- C) Yes, but only in Java, not Python.
Answer: B

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
SFT for LLMs: A Practical Guide to Supervised Fine-Tuning
TLDR: Supervised fine-tuning (SFT) is the stage where a pretrained model learns task-specific response behavior from curated input-output examples. It is usually the first alignment step after pretraining and often the foundation for later RLHF. Good...
RLHF in Practice: From Human Preferences to Better LLM Policies
TLDR: Reinforcement Learning from Human Feedback (RLHF) helps align language models with human preferences after pretraining and SFT. The typical pipeline is: collect preference comparisons, train a reward model, then optimize a policy (often with KL...
PEFT, LoRA, and QLoRA: A Practical Guide to Efficient LLM Fine-Tuning
TLDR: Full fine-tuning updates every model weight, which is expensive in memory, compute, and storage. PEFT methods update only a small trainable slice. LoRA learns low-rank adapters on top of frozen base weights. QLoRA pushes efficiency further by q...
LLM Model Naming Conventions: How to Read Names and Why They Matter
TLDR: LLM names encode practical decisions: model family, size, training stage, context window, format, and quantization level. If you can decode naming conventions, you can avoid costly deployment mistakes and choose the right checkpoint faster. ๏ฟฝ...
