💡 What is the Open/Closed Principle?
The Open/Close Principle (OCP) is one of the five SOLID principles of object-oriented design, coined by Bertrand Meyer. It states
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modifications”
.
You should be able to extend the behavior of a module without modifying its source code.
In simple terms:
Your code should allow new behavior to be added without altering the existing source code. This makes your applications more flexible, stable, and easier to maintain.
🔑 Key Concepts of OCP
Concept | Description |
---|---|
Open for Extension | You can add new functionality to the module or class. |
Closed for Modification | Existing, tested code remains untouched while adding new features. |
This principle is most effectively achieved through abstraction, polymorphism, and composition over inheritance.
🧱 Core Idea
Design your classes so you can add new functionality by writing new code—not by editing old code.
💡 How to achieve this:
- Use interfaces or abstract base classes
- Apply polymorphism to defer decisions to runtime
- Prefer composition over inheritance where appropriate
🧠 Why Does This Matter?
Imagine you're working on a large codebase. Every time there's a new requirement, you have to dive into existing classes and modify them. Not only does this risk introducing bugs, but it also breaks the stability of a system that might already be in production.
The Open/Closed Principle helps by allowing you to extend functionality without touching the core logic that's already been tested and verified.
🛠 Real-World Analogy
🔌 Power Strip Analogy:
You can plug in more devices (extend functionality) without modifying how the strip is built. That’s OCP—extension without modification.
🚫 Violation of OCP – Payment Processor Example
class PaymentProcessor {
public:
void processPayment(std::string method) {
if (method == "credit_card") {
// Process credit card payment
} else if (method == "paypal") {
// Process PayPal payment
}
}
};
👉 Here, if we want to support a new method (say, Bitcoin), we need to modify the processPayment
method. This violates OCP.
✅ Refactored Using OCP
Let's apply the Open/Closed Principle
using polymorphism.
class PaymentMethod {
public:
virtual void pay() = 0;
};
class CreditCard : public PaymentMethod {
public:
void pay() override {
// Process credit card
}
};
class PayPal : public PaymentMethod {
public:
void pay() override {
// Process PayPal
}
};
class PaymentProcessor {
public:
void process(PaymentMethod* method) {
method->pay();
}
};
➕ Now, if we want to add a new payment type (like Bitcoin
), we just create a new class:
class Bitcoin : public PaymentMethod {
public:
void pay() override {
// Process Bitcoin
}
};
👉 No changes needed in the PaymentProcessor
class. That’s the Open/Closed Principle in action.
📩 Notification System Example
❌ Violating OCP
Suppose we are building a notification system where users can receive messages via email. Here’s an implementation:
class Notification {
public:
void sendEmail(const string& message) {
cout << "Sending Email: " << message << endl;
}
};
👉 Now, if we want to add SMS notifications, we’ll need to modify the Notification
class:
class Notification {
public:
void sendEmail(const string& message) {
cout << "Sending Email: " << message << endl;
}
void sendSMS(const string& message) {
cout << "Sending SMS: " << message << endl;
}
};
With every new notification type (e.g., push notifications, Slack messages), this class will require modification, violating the closed for modification rule.
✅ Following OCP
To adhere to OCP, we can use polymorphism and rely on an abstract base class or interface. Here's the refactored code:
// Abstract base class
class Notification {
public:
virtual void send(const string& message) = 0; // Abstract method
virtual ~Notification() = default;
};
// Concrete implementation: EmailNotification
class EmailNotification : public Notification {
public:
void send(const string& message) override {
cout << "Sending Email: " << message << endl;
}
};
// Concrete implementation: SMSNotification
class SMSNotification : public Notification {
public:
void send(const string& message) override {
cout << "Sending SMS: " << message << endl;
}
};
// Client code
class NotificationService {
private:
Notification& notification;
public:
NotificationService(Notification& notif) : notification(notif) {}
void notify(const string& message) {
notification.send(message); // No knowledge of how the message is sent
}
};
Usage:
int main() {
EmailNotification email;
SMSNotification sms;
NotificationService emailService(email);
emailService.notify("Hello via Email!"); // Output: Sending Email: Hello via Email!
NotificationService smsService(sms);
smsService.notify("Hello via SMS!"); // Output: Sending SMS: Hello via SMS!
return 0;
}
How OCP Works Here
- Closed for Modification: The
NotificationService
class doesn’t need to be modified when a new notification type is added. - Open for Extension: To add a new notification type (e.g., push notifications), you simply create a new class that implements the
Notification
interface.
🧮 Another Practical Example: Shape Area Calculator
Consider a scenario where you have a Shape
class hierarchy in a drawing application, consisting of various shapes like Circle
, Rectangle
, and Triangle
. Initially, you might have a method calculateArea()
in each shape class to compute its area. Let's see how you can adhere to the Open/Closed Principle in this scenario:
#include <iostream>
class Shape {
public:
virtual double calculateArea() const = 0; // Pure virtual function
virtual ~Shape() {} // Virtual destructor
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double calculateArea() const override {
return 3.14 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() const override {
return width * height;
}
};
// Suppose we want to add a new shape, Triangle, without modifying existing code.
class Triangle : public Shape {
private:
double base;
double height;
public:
Triangle(double b, double h) : base(b), height(h) {}
double calculateArea() const override {
return 0.5 * base * height;
}
};
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.calculateArea() << std::endl;
}
int main() {
Circle circle(5);
Rectangle rectangle(4, 6);
Triangle triangle(3, 4);
printArea(circle); // Output: Area: 78.5
printArea(rectangle); // Output: Area: 24
printArea(triangle); // Output: Area: 6
return 0;
}
In this example, the Shape
class is an abstract base class with a pure virtual function calculateArea()
. The function needs to be implemented by any derived class representing a specific shape. Each concrete shape class (Circle
, Rectangle
, Triangle
) provides its own implementation of calculateArea()
.
Now, suppose we want to add a new shape, Triangle
, to our application. We can do so without modifying the existing codebase. We simple create a new Triangle
class that inherits from Shape
and implements its own calculateArea()
method.
🎯 Practical Applications of the Open/Closed Principle
- Inheritance and Polymorphism: Leveraging inheritance and polymorphism allows developers to create base classes that define common behaviors and interfaces. Derived classes can then extend these functionalities or override specific methods to introduce new behaviors, all while adhering to the OCP.
- Design Patterns: Many design patterns, such as the Strategy Pattern and the Observer Pattern, embody the principles of OCP. These patterns encapsulate algorithms, behaviors, or responsibilities into separate classes, making it easy to extend or modify their functionality without altering the core components.
- Plugin Architecture: Adopting a plugin-based architecture enables applications to be extended dynamically at runtime. Plugins can be developed independently, following the OCP, and seamlessly integrated into the existing system without requiring modifications to the core codebase.
🧩 How to Achieve OCP in Practice
- ✅ Use abstract classes or interfaces
- ✅ Leverage polymorphism
- ✅ Apply design patterns like Strategy, Decorator, and Factory
- ❌ Avoid type-checking or switch-case statements for extensible behavior
✔ Key Techniques to Ensure OCP
Technique | Description |
---|---|
Use abstract classes or interfaces | Define a common behavior for extension |
Use inheritance or composition | Add new behavior via derived or composed classes |
Avoid tight coupling | Helps prevent ripple modifications across the codebase |
Avoid if-else /switch-case ladders | Replace them with polymorphic behavior for extensibility |
📍 When Should You Apply OCP?
- When a class is frequently modified for new features
- When you anticipate future extensibility
- When working in agile/CI-CD environments
- When you want to improve modularity and maintainability
🚀 Benefits of Following OCP
- ✅ Stability – No need to alter tested code
✅ Scalability – Easy to add new features
✅ Maintainability – Clean, modular design
✅ Flexibility – Respond to changing requirements quickly