SOLID Principles in Depth: The Open/Closed Principle (OCP)
In our journey through the SOLID principles of object-oriented design, we’ve explored the Single Responsibility Principle (SRP)—a foundation for clean and maintainable code. Now, let’s dive into the Open/Closed Principle (OCP), a rule that helps your system grow without breaking existing code.
What is the Open/Closed Principle?
Definition (Bertrand Meyer, 1988):
"Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification."
In simple terms:
You should be able to extend a class’s behavior without changing its existing code.
This principle encourages developers to write code that is resilient to change. Instead of modifying existing, working code every time a requirement changes, you extend it via inheritance, composition, or polymorphism.
Why OCP Matters
- Minimizes risk – Changing existing code can break functionality.
- Improves maintainability – Extensions can be added without touching tested components.
- Encourages modular design – Clean separation of core logic and variant behavior.
Real-World Analogy: Power Plug Adapters
Think of your laptop charger. When traveling to different countries, you don’t throw away the charger and build a new one—you just add an adapter.
The charger (core logic) stays the same. The adapter (extension) makes it compatible with a different system.
That’s OCP in action: extend behavior without modifying the original.
Code Example: OCP Violation
Suppose you’re building a notification system:
class NotificationService: def send(self, message, type): if type == "email": self.send_email(message) elif type == "sms": self.send_sms(message) def send_email(self, message): print(f"Sending Email: {message}") def send_sms(self, message): print(f"Sending SMS: {message}")
Now your PM says: “We need to support push notifications.”
You'd need to modify the class, which risks breaking existing logic—a clear violation of OCP.
Applying OCP: Refactored Design
Let’s refactor it using polymorphism:
class Notifier: def send(self, message): raise NotImplementedError class EmailNotifier(Notifier): def send(self, message): print(f"Sending Email: {message}") class SMSNotifier(Notifier): def send(self, message): print(f"Sending SMS: {message}") class PushNotifier(Notifier): def send(self, message): print(f"Sending Push Notification: {message}") class NotificationService: def __init__(self, notifiers): self.notifiers = notifiers def send_all(self, message): for notifier in self.notifiers: notifier.send(message)
Now, to add push notifications, you don’t touch
Real-World Example: E-commerce Discount Strategy
In an e-commerce app:
class DiscountService { public double calculateDiscount(Order order, String customerType) { if (customerType.equals("REGULAR")) { return order.getTotal() * 0.05; } else if (customerType.equals("PREMIUM")) { return order.getTotal() * 0.10; } return 0; } }
This is fine—until marketing wants to add more customer types, seasonal discounts, coupons, etc.
Every new rule means modifying this class, violating OCP.
OCP-Compliant Design:
interface DiscountStrategy { double getDiscount(Order order); } class RegularCustomerDiscount implements DiscountStrategy { public double getDiscount(Order order) { return order.getTotal() * 0.05; } } class PremiumCustomerDiscount implements DiscountStrategy { public double getDiscount(Order order) { return order.getTotal() * 0.10; } } class DiscountService { public double calculateDiscount(Order order, DiscountStrategy strategy) { return strategy.getDiscount(order); } }
Now you can plug in any new discount strategy without ever modifying
Tools to Help Practice OCP
- Interfaces & Abstract Classes – Define stable contracts for extension.
- Composition over Inheritance – Prefer injecting behavior over overriding it.
- Strategy & Factory Patterns – Common design patterns that support OCP.
Pro Tips
- If your iforswitchstatements are growing with every new feature, it's a red flag.
- Use interfaces or base classes as extension points.
- Test extensions independently from the core logic.
Conclusion
The Open/Closed Principle encourages you to think ahead—structure your code not just for what it does now, but how it might evolve tomorrow. By designing for extension instead of modification, you future-proof your applications and build systems that are flexible, scalable, and safe to change.
Up Next: The Liskov Substitution Principle—don’t miss it!