The Open/Closed Principle says: software entities (classes, modules, functions) should be open for extension but closed for modification.
In simple language, we should be able to add new behavior without changing existing code. Sounds impossible? It’s not — we use polymorphism.
The Problem: The If/Else Chain
Let’s say we’re building a payment system. Here’s the naive approach:
# BAD -- adding a new payment type means modifying this class
class PaymentProcessor:
def process(self, payment_type, amount):
if payment_type == "credit_card":
print(f"Processing ${amount} via Credit Card")
# credit card logic...
elif payment_type == "upi":
print(f"Processing ${amount} via UPI")
# UPI logic...
elif payment_type == "paypal":
print(f"Processing ${amount} via PayPal")
# PayPal logic...
# Every new payment type = modify this method
// BAD -- adding a new payment type means modifying this class
class PaymentProcessor {
process(paymentType, amount) {
if (paymentType === "credit_card") {
console.log(`Processing $${amount} via Credit Card`);
} else if (paymentType === "upi") {
console.log(`Processing $${amount} via UPI`);
} else if (paymentType === "paypal") {
console.log(`Processing $${amount} via PayPal`);
}
// Every new payment type = modify this method
}
}
// BAD -- adding a new payment type means modifying this class
class PaymentProcessor {
void process(String paymentType, double amount) {
if (paymentType.equals("credit_card")) {
System.out.println("Processing $" + amount + " via Credit Card");
} else if (paymentType.equals("upi")) {
System.out.println("Processing $" + amount + " via UPI");
} else if (paymentType.equals("paypal")) {
System.out.println("Processing $" + amount + " via PayPal");
}
// Every new payment type = modify this method
}
}
Want to add crypto payments? We have to open up PaymentProcessor and add another elif. That’s modification of existing, working code. Every time we touch it, we risk breaking what already works.
The Fix: Use Polymorphism
Instead of a big if/else, we define a common interface and let each payment type implement it:
from abc import ABC, abstractmethod
# The abstraction -- closed for modification
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount):
pass
# Open for extension -- just add new classes
class CreditCardPayment(PaymentMethod):
def process(self, amount):
print(f"Charging ${amount} to credit card")
class UPIPayment(PaymentMethod):
def process(self, amount):
print(f"Paying ${amount} via UPI")
class CryptoPayment(PaymentMethod): # NEW -- no existing code changed!
def process(self, amount):
print(f"Sending ${amount} in crypto")
class PaymentProcessor:
def process(self, method: PaymentMethod, amount: float):
method.process(amount)
# Usage
processor = PaymentProcessor()
processor.process(CryptoPayment(), 100) # works without touching processor
// The abstraction
class PaymentMethod {
process(amount) {
throw new Error("Must implement process()");
}
}
// Open for extension -- just add new classes
class CreditCardPayment extends PaymentMethod {
process(amount) {
console.log(`Charging $${amount} to credit card`);
}
}
class UPIPayment extends PaymentMethod {
process(amount) {
console.log(`Paying $${amount} via UPI`);
}
}
class CryptoPayment extends PaymentMethod {
// NEW -- no existing code changed!
process(amount) {
console.log(`Sending $${amount} in crypto`);
}
}
class PaymentProcessor {
process(method, amount) {
method.process(amount);
}
}
const processor = new PaymentProcessor();
processor.process(new CryptoPayment(), 100);
// The abstraction -- closed for modification
interface PaymentMethod {
void process(double amount);
}
// Open for extension -- just add new classes
class CreditCardPayment implements PaymentMethod {
public void process(double amount) {
System.out.println("Charging $" + amount + " to credit card");
}
}
class UPIPayment implements PaymentMethod {
public void process(double amount) {
System.out.println("Paying $" + amount + " via UPI");
}
}
class CryptoPayment implements PaymentMethod { // NEW!
public void process(double amount) {
System.out.println("Sending $" + amount + " in crypto");
}
}
class PaymentProcessor {
void process(PaymentMethod method, double amount) {
method.process(amount);
}
}
Now adding a new payment type means creating a new class. We never touch PaymentProcessor or any existing payment class. Open for extension, closed for modification.
Another Example: Discount Calculator
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, price):
pass
class NoDiscount(DiscountStrategy):
def calculate(self, price):
return price
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent):
self.percent = percent
def calculate(self, price):
return price * (1 - self.percent / 100)
class FlatDiscount(DiscountStrategy):
def __init__(self, flat_amount):
self.flat_amount = flat_amount
def calculate(self, price):
return max(0, price - self.flat_amount)
# Adding "BuyOneGetOneFree" = just a new class. Zero modifications.
def checkout(price, discount: DiscountStrategy):
final_price = discount.calculate(price)
print(f"Original: ${price}, Final: ${final_price}")
checkout(100, PercentageDiscount(20)) # Original: $100, Final: $80.0
checkout(100, FlatDiscount(30)) # Original: $100, Final: $70
class NoDiscount {
calculate(price) {
return price;
}
}
class PercentageDiscount {
constructor(percent) {
this.percent = percent;
}
calculate(price) {
return price * (1 - this.percent / 100);
}
}
class FlatDiscount {
constructor(flatAmount) {
this.flatAmount = flatAmount;
}
calculate(price) {
return Math.max(0, price - this.flatAmount);
}
}
function checkout(price, discount) {
const finalPrice = discount.calculate(price);
console.log(`Original: $${price}, Final: $${finalPrice}`);
}
checkout(100, new PercentageDiscount(20)); // Final: $80
checkout(100, new FlatDiscount(30)); // Final: $70
interface DiscountStrategy {
double calculate(double price);
}
class NoDiscount implements DiscountStrategy {
public double calculate(double price) { return price; }
}
class PercentageDiscount implements DiscountStrategy {
double percent;
PercentageDiscount(double percent) { this.percent = percent; }
public double calculate(double price) {
return price * (1 - percent / 100);
}
}
class FlatDiscount implements DiscountStrategy {
double flatAmount;
FlatDiscount(double flatAmount) { this.flatAmount = flatAmount; }
public double calculate(double price) {
return Math.max(0, price - flatAmount);
}
}
The Pattern
OCP almost always follows the same recipe:
- Define an abstraction (interface or abstract class)
- Write concrete implementations for each variant
- Depend on the abstraction in consuming code
This is also the core of the Strategy Pattern — one of the most useful design patterns in LLD interviews.
When We Can’t Avoid Modification
Let’s be real — sometimes we HAVE to modify existing code. OCP is a guideline, not a religion. If a change is small and localized, it’s fine. OCP shines when we have a growing family of types (payment methods, discount rules, notification channels) where new additions are frequent.
The goal: design our code so that the most common changes require the least modification of existing code.