Overview
The Open/Closed Principle states that software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
Definition
Software entities should be open for extension but closed for modification.
- Open for extension: You can add new functionality
- Closed for modification: You don’t need to change existing code
Key Points:
- Add new features via extension (inheritance, interfaces, composition)
- Don’t modify existing, working code
- Reduces risk of breaking changes
Beginner Level
❌ Bad Example (Violates OCP)
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
return rect.width * rect.height;
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
}
// To add Triangle, we need to modify this class!
throw new IllegalArgumentException("Unknown shape");
}
}
Problem: To add a new shape (Triangle), we must modify AreaCalculator.
✅ Good Example (Follows OCP)
// Base interface - closed for modification
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
private double width;
private double height;
@Override
public double calculateArea() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// To add Triangle, we just implement Shape - no modification needed!
public class Triangle implements Shape {
private double base;
private double height;
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea(); // Works with any Shape!
}
}
Benefits:
- ✅ Add new shapes without modifying
AreaCalculator - ✅ Existing code remains unchanged
- ✅ No risk of breaking existing functionality
Intermediate Level
Worked example
✅ Strategy + registry: payment methods
Closed for modification of the checkout path; open for new payment methods.
public interface PaymentMethod {
String id(); // "card", "upi", "wallet"
PaymentResult pay(Money amount);
}
public class CardPayment implements PaymentMethod {
@Override public String id() { return "card"; }
@Override public PaymentResult pay(Money amount) { /* charge card */ return PaymentResult.ok(); }
}
public class UpiPayment implements PaymentMethod {
@Override public String id() { return "upi"; }
@Override public PaymentResult pay(Money amount) { /* UPI collect */ return PaymentResult.ok(); }
}
public class PaymentMethodRegistry {
private final Map<String, PaymentMethod> byId = new HashMap<>();
public PaymentMethodRegistry(List<PaymentMethod> methods) {
for (PaymentMethod method : methods) {
byId.put(method.id(), method);
}
}
public PaymentMethod get(String id) {
PaymentMethod method = byId.get(id);
if (method == null) {
throw new IllegalArgumentException("Unknown payment method: " + id);
}
return method;
}
}
public class CheckoutService {
private final PaymentMethodRegistry payments;
public CheckoutService(PaymentMethodRegistry payments) {
this.payments = payments;
}
public void pay(Order order, String methodId) {
payments.get(methodId).pay(order.total());
}
}
Why this follows OCP:
- ✅ Adding wallet pay = new
PaymentMethodclass + register it - ✅
CheckoutServiceandPaymentMethodRegistrystay untouched - ✅ Existing card/UPI behavior is not edited to make room for wallet
Adding a new method:
public class WalletPayment implements PaymentMethod {
@Override public String id() { return "wallet"; }
@Override public PaymentResult pay(Money amount) { /* debit wallet */ return PaymentResult.ok(); }
}
// Construct registry with CardPayment, UpiPayment, WalletPayment — done.
Template Method: report exporters
public abstract class ReportExporter {
public final byte[] export(Report report) {
validate(report);
Object model = transform(report); // extension point
return serialize(model); // extension point
}
protected void validate(Report report) {
if (report == null) throw new IllegalArgumentException("report required");
}
protected abstract Object transform(Report report);
protected abstract byte[] serialize(Object model);
}
public class CsvReportExporter extends ReportExporter {
@Override protected Object transform(Report report) { /* rows */ return List.of(); }
@Override protected byte[] serialize(Object model) { /* CSV bytes */ return new byte[0]; }
}
New formats extend ReportExporter. The export skeleton stays closed.
Advanced Level
Extension Points
1. Inheritance (Template Method Pattern)
public abstract class DataProcessor {
// Template method - closed for modification
public final void process() {
validate();
transform();
save();
notify();
}
// Extension points - open for extension
protected abstract void validate();
protected abstract void transform();
protected abstract void save();
// Hook method - can be overridden
protected void notify() {
// Default implementation
}
}
2. Composition (Strategy Pattern)
public class PaymentProcessor {
private PaymentStrategy strategy; // Interface
public void processPayment(Amount amount) {
strategy.pay(amount); // Can use any strategy
}
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy; // Change behavior without modifying class
}
}
3. Plugin Architecture
// New PaymentMethod implementations are discovered and registered once
public PaymentMethodRegistry(List<PaymentMethod> methods) {
for (PaymentMethod method : methods) {
byId.put(method.id(), method);
}
}
Design Patterns that Support OCP
- Factory Pattern - Add new products without modifying factory
- Strategy Pattern - Add new strategies without modifying context
- Template Method - Add new implementations without modifying template
- Decorator Pattern - Add new behaviors without modifying existing classes
- Observer Pattern - Add new observers without modifying subject
Best Practices
✅ DO:
-
Use interfaces and abstract classes
public interface PaymentProcessor { void process(Payment payment); } -
Design for extension points
public abstract class BaseProcessor { public final void process() { // Common logic doProcess(); // Extension point } protected abstract void doProcess(); } -
Use dependency injection
public class OrderService { private PaymentProcessor processor; // Can be any implementation } -
Use factory patterns
public class ServiceFactory { public Service getService(Type type) { // Returns appropriate implementation } }
❌ DON’T:
-
Don’t use if-else chains for type checking
// ❌ Bad if (type == Type.A) { // Handle A } else if (type == Type.B) { // Handle B } -
Don’t modify existing code to add features
// ❌ Bad: Modifying existing method public void processOrder(Order order) { // Existing logic if (order.getType() == NEW_TYPE) { // Adding new condition // New logic } } -
Don’t use switch statements for behavior
// ❌ Bad switch (type) { case A: // handle A case B: // handle B }
Interview Questions
Beginner Level
Q1: What is Open/Closed Principle? Answer: Software should be open for extension but closed for modification. You should be able to add new features without changing existing code.
Q2: What’s the difference between “open” and “closed”? Answer:
- Closed: Existing code doesn’t change
- Open: New functionality can be added via extension (inheritance, interfaces, composition)
Q3: How do you achieve OCP in practice? Answer:
- Use interfaces and abstract classes
- Use factory patterns
- Use strategy pattern
- Use dependency injection
- Design for extension points
Intermediate Level
Q4: Can you give a concrete OCP example?
Answer: A PaymentMethod interface with card/UPI implementations and a registry. Adding wallet pay is a new class registered into the map — CheckoutService never gains another if (method.equals(...)) branch.
Q5: Can you violate OCP with inheritance? Answer: Yes! If you override methods and change base behavior, you’re modifying. OCP means extending behavior, not changing it.
Q6: What design patterns support OCP? Answer:
- Factory Pattern
- Strategy Pattern
- Template Method Pattern
- Decorator Pattern
- Observer Pattern
Advanced Level
Q7: How does OCP relate to other SOLID principles? Answer:
- OCP often requires DIP (depend on abstractions)
- OCP benefits from SRP (focused classes are easier to extend)
- OCP works with LSP (subtypes must be substitutable)
Q8: What’s the trade-off with OCP? Answer:
- Pros: Less risk of breaking changes, easier to add features
- Cons: Can lead to more abstractions, might be over-engineered for simple cases
Q9: How do you refactor code to follow OCP? Answer:
- Identify areas that change frequently
- Extract interfaces/abstract classes
- Use factory/strategy patterns
- Replace if-else chains with polymorphism
- Test after refactoring
Summary
Key Points:
- ✅ Open for extension (add new features)
- ✅ Closed for modification (don’t change existing code)
- ✅ Use interfaces, abstract classes, and patterns
- ✅ Design for extension points
Quick Reference:
OCP: Software should be open for extension but closed for modification
Next Steps:
- Review your codebase for OCP violations
- Identify areas that require modification for new features
- Refactor to use interfaces and patterns
- Practice explaining OCP with examples
Previous: Single Responsibility Principle | Next: Liskov Substitution Principle