Skip to content

🧑‍💻 Author: RK ROY

Open/Closed Principle (OCP)

🎯 Definition

"Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification."

- Bertrand Meyer

The Open/Closed Principle states that you should be able to extend the behavior of a system without modifying its existing code. This is achieved through abstraction and polymorphism.

🤔 What Does "Open/Closed" Mean?

  • Open for Extension: You can add new functionality and behaviors
  • Closed for Modification: Existing code should not be changed when adding new features

Key Concepts

❌ OCP Violation: Modification Required

Let's look at a classic example that violates OCP:

cpp
#include <iostream>
#include <string>
using namespace std;

// ❌ VIOLATION: Need to modify existing code for new features
class DiscountCalculator {
public:
    double calculateDiscount(const string& customerType, double amount) {
        if (customerType == "REGULAR") {
            return amount * 0.05;
        } else if (customerType == "PREMIUM") {
            return amount * 0.10;
        } else if (customerType == "VIP") {
            return amount * 0.15;
        } else if (customerType == "GOLD") { // ❌ New feature requires modification
            return amount * 0.20;
        } else if (customerType == "PLATINUM") {
            return amount * 0.25;
        }
        return 0;
    }
};

Problems with This Design

  1. Modification Required: Every new customer type requires changing existing code
  2. Risk of Bugs: Modifying existing code can introduce bugs
  3. Violation of Single Responsibility: The class has to know about all customer types
  4. Hard to Test: Need to retest all existing functionality when adding new types
  5. Tight Coupling: Calculator is tightly coupled to specific customer types

✅ OCP Solution: Extension Without Modification

Let's refactor using abstraction and polymorphism:

1. Create Discount Strategy Interface

cpp
#include <iostream>
#include <string>
#include <map>
#include <memory>
#include <stdexcept>
#include <set>
using namespace std;

class DiscountStrategy {
public:
    virtual double calculateDiscount(double amount) const = 0;
    virtual string getCustomerType() const = 0;
    virtual ~DiscountStrategy() = default;
};

2. Implement Concrete Strategies

cpp
class RegularCustomerDiscount : public DiscountStrategy {
public:
    double calculateDiscount(double amount) const override {
        return amount * 0.05;
    }
    string getCustomerType() const override {
        return "REGULAR";
    }
};

class PremiumCustomerDiscount : public DiscountStrategy {
public:
    double calculateDiscount(double amount) const override {
        return amount * 0.10;
    }
    string getCustomerType() const override {
        return "PREMIUM";
    }
};

class VipCustomerDiscount : public DiscountStrategy {
public:
    double calculateDiscount(double amount) const override {
        return amount * 0.15;
    }
    string getCustomerType() const override {
        return "VIP";
    }
};

3. Context Class Using Strategy

cpp
class DiscountCalculator {
    map<string, unique_ptr<DiscountStrategy>> strategies;
public:
    DiscountCalculator() {
        registerStrategy(make_unique<RegularCustomerDiscount>());
        registerStrategy(make_unique<PremiumCustomerDiscount>());
        registerStrategy(make_unique<VipCustomerDiscount>());
    }

    void registerStrategy(unique_ptr<DiscountStrategy> strategy) {
        string key = strategy->getCustomerType();
        strategies[key] = move(strategy);
    }

    double calculateDiscount(const string& customerType, double amount) const {
        auto it = strategies.find(customerType);
        if (it == strategies.end()) {
            throw invalid_argument("Unknown customer type: " + customerType);
        }
        return it->second->calculateDiscount(amount);
    }

    set<string> getSupportedCustomerTypes() const {
        set<string> keys;
        for (auto& kv : strategies)
            keys.insert(kv.first);
        return keys;
    }
};

🚀 Adding New Features Without Modification

Now we can add new customer types without modifying existing code:

cpp
class GoldCustomerDiscount : public DiscountStrategy {
public:
    double calculateDiscount(double amount) const override {
        return amount * 0.20;
    }
    string getCustomerType() const override {
        return "GOLD";
    }
};

class PlatinumCustomerDiscount : public DiscountStrategy {
public:
    double calculateDiscount(double amount) const override {
        if (amount > 10000) return amount * 0.30;
        else if (amount > 5000) return amount * 0.25;
        return amount * 0.20;
    }
    string getCustomerType() const override {
        return "PLATINUM";
    }
};

class CorporateCustomerDiscount : public DiscountStrategy {
    bool isHolidaySeason;
public:
    CorporateCustomerDiscount(bool isHoliday) : isHolidaySeason(isHoliday) {}
    double calculateDiscount(double amount) const override {
        double base = amount * 0.18;
        return isHolidaySeason ? base * 1.5 : base;
    }
    string getCustomerType() const override {
        return "CORPORATE";
    }
};

🏗️ Class Diagram After OCP

🎯 Using the OCP Solution

cpp
int main() {
    DiscountCalculator calculator;

    calculator.registerStrategy(make_unique<GoldCustomerDiscount>());
    calculator.registerStrategy(make_unique<PlatinumCustomerDiscount>());
    calculator.registerStrategy(make_unique<CorporateCustomerDiscount>(true));

    double amount = 1000.0;

    cout << "Regular: $" << calculator.calculateDiscount("REGULAR", amount) << endl;
    cout << "Premium: $" << calculator.calculateDiscount("PREMIUM", amount) << endl;
    cout << "VIP: $" << calculator.calculateDiscount("VIP", amount) << endl;
    cout << "Gold: $" << calculator.calculateDiscount("GOLD", amount) << endl;
    cout << "Platinum: $" << calculator.calculateDiscount("PLATINUM", amount) << endl;
    cout << "Corporate: $" << calculator.calculateDiscount("CORPORATE", amount) << endl;

    cout << "\nSupported customer types: ";
    for (auto& type : calculator.getSupportedCustomerTypes())
        cout << type << " ";
    cout << endl;
}

🎨 More OCP Examples

Example 1: Shape Area Calculator

❌ Violation

cpp
#include <typeinfo>
#include <cmath>

class Circle { public: double r; Circle(double radius): r(radius) {} };
class Rectangle { public: double w, h; Rectangle(double w_, double h_): w(w_), h(h_) {} };
class Triangle { public: double b, h; Triangle(double b_, double h_): b(b_), h(h_) {} };

class AreaCalculator {
public:
    double calculateArea(const Circle& c) { return M_PI * c.r * c.r; }
    double calculateArea(const Rectangle& r) { return r.w * r.h; }
    double calculateArea(const Triangle& t) { return 0.5 * t.b * t.h; } // ❌ New code modification
};

✅ Solution

cpp
class Shape {
public:
    virtual double calculateArea() const = 0;
    virtual ~Shape() = default;
};

class CircleShape : public Shape {
    double radius;
public:
    CircleShape(double r): radius(r) {}
    double calculateArea() const override { return M_PI * radius * radius; }
};

class RectangleShape : public Shape {
    double width, height;
public:
    RectangleShape(double w, double h): width(w), height(h) {}
    double calculateArea() const override { return width * height; }
};

class TriangleShape : public Shape {
    double base, height;
public:
    TriangleShape(double b, double h): base(b), height(h) {}
    double calculateArea() const override { return 0.5 * base * height; }
};

#include <vector>
#include <numeric>

class AreaCalculator {
public:
    double calculateTotalArea(const vector<unique_ptr<Shape>>& shapes) const {
        double total = 0;
        for (auto& s : shapes) total += s->calculateArea();
        return total;
    }
};

Example 2: Payment Processing

❌ Violation

cpp
#include <iostream>
#include <string>
using namespace std;

class PaymentProcessor {
public:
    void processPayment(const string& paymentType, double amount) {
        if (paymentType == "CREDIT_CARD") {
            // Credit card processing logic
            cout << "Processing credit card payment: $" << amount << '\n';
        } else if (paymentType == "PAYPAL") {
            // PayPal processing logic
            cout << "Processing PayPal payment: $" << amount << '\n';
        } else if (paymentType == "CRYPTO") { // ❌ NEW: Modification required
            // Cryptocurrency processing logic
            cout << "Processing crypto payment: $" << amount << '\n';
        }
    }
};

int main() {
    PaymentProcessor p;
    p.processPayment("CREDIT_CARD", 100.0);
    p.processPayment("PAYPAL", 200.0);
    p.processPayment("CRYPTO", 300.0);
}

✅ Solution

cpp
#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <algorithm>

class PaymentStrategy {
public:
    virtual void processPayment(double amount) const = 0;
    virtual string getPaymentType() const = 0;
    virtual bool isAvailable() const = 0;
    virtual ~PaymentStrategy() = default;
};

class CreditCardPayment : public PaymentStrategy {
    string cardNumber, expiry;
public:
    CreditCardPayment(string c, string e): cardNumber(c), expiry(e) {}
    void processPayment(double amount) const override {
        cout << "Processing credit card payment: $" << amount << endl;
        cout << "Card ending: " << cardNumber.substr(cardNumber.size() - 4) << endl;
    }
    string getPaymentType() const override { return "CREDIT_CARD"; }
    bool isAvailable() const override { return true; }
};

class PayPalPayment : public PaymentStrategy {
    string email;
public:
    PayPalPayment(string e): email(e) {}
    void processPayment(double amount) const override {
        cout << "Processing PayPal payment: $" << amount << endl;
        cout << "Account: " << email << endl;
    }
    string getPaymentType() const override { return "PAYPAL"; }
    bool isAvailable() const override { return true; }
};

class CryptoPayment : public PaymentStrategy {
    string wallet, coin;
public:
    CryptoPayment(string w, string c): wallet(w), coin(c) {}
    void processPayment(double amount) const override {
        cout << "Processing " << coin << " payment: $" << amount << endl;
        cout << "Wallet: " << wallet << endl;
    }
    string getPaymentType() const override { return "CRYPTO"; }
    bool isAvailable() const override { return true; }
};

class PaymentProcessor {
    map<string, unique_ptr<PaymentStrategy>> methods;
public:
    void registerPayment(unique_ptr<PaymentStrategy> method) {
        methods[method->getPaymentType()] = move(method);
    }

    void processPayment(const string& type, double amount) {
        auto it = methods.find(type);
        if (it == methods.end()) throw invalid_argument("Unsupported payment type: " + type);
        if (!it->second->isAvailable()) throw runtime_error("Payment method not available");
        it->second->processPayment(amount);
    }

    vector<string> getAvailableMethods() const {
        vector<string> result;
        for (auto& m : methods)
            if (m.second->isAvailable()) result.push_back(m.first);
        return result;
    }
};

🛠️ OCP Implementation Patterns

1. Strategy Pattern

cpp
#include <iostream>
#include <string>
#include <memory>
#include <algorithm>
using namespace std;

// Strategy interface
class FormattingStrategy {
public:
    virtual string format(const string& text) const = 0;
    virtual ~FormattingStrategy() = default;
};

// Concrete strategy — UpperCase
class UpperCaseFormatter : public FormattingStrategy {
public:
    string format(const string& text) const override {
        string result = text;
        transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }
};

// Concrete strategy — LowerCase
class LowerCaseFormatter : public FormattingStrategy {
public:
    string format(const string& text) const override {
        string result = text;
        transform(result.begin(), result.end(), result.begin(), ::tolower);
        return result;
    }
};

// Context class that uses strategies
class TextFormatter {
    unique_ptr<FormattingStrategy> strategy;
public:
    void setFormattingStrategy(unique_ptr<FormattingStrategy> s) {
        strategy = move(s);
    }

    string format(const string& text) const {
        if (!strategy) throw runtime_error("No formatting strategy set!");
        return strategy->format(text);
    }
};

// Example usage
int main() {
    TextFormatter formatter;

    formatter.setFormattingStrategy(make_unique<UpperCaseFormatter>());
    cout << "Upper: " << formatter.format("Open Closed Principle") << endl;

    formatter.setFormattingStrategy(make_unique<LowerCaseFormatter>());
    cout << "Lower: " << formatter.format("Open Closed Principle") << endl;
}

2. Template Method Pattern

cpp
#include <iostream>
#include <string>
using namespace std;

// Abstract base class defining the algorithm template
class DataProcessor {
public:
    // Template method - defines the algorithm structure
    void processData(const string& data) final {
        string cleanedData = cleanData(data);
        string validatedData = validateData(cleanedData);
        string transformedData = transformData(validatedData);
        saveData(transformedData);
    }

protected:
    // Steps to be implemented by subclasses
    virtual string cleanData(const string& data) = 0;
    virtual string validateData(const string& data) = 0;
    virtual string transformData(const string& data) = 0;
    virtual void saveData(const string& data) = 0;

    virtual ~DataProcessor() = default;
};

// ✅ NEW: CSVDataProcessor subclass
class CSVDataProcessor : public DataProcessor {
protected:
    string cleanData(const string& data) override {
        cout << "Cleaning CSV data...\n";
        return data;
    }

    string validateData(const string& data) override {
        cout << "Validating CSV data...\n";
        return data;
    }

    string transformData(const string& data) override {
        cout << "Transforming CSV data...\n";
        return data;
    }

    void saveData(const string& data) override {
        cout << "Saving CSV data...\n";
    }
};

// ✅ NEW: JSONDataProcessor subclass
class JSONDataProcessor : public DataProcessor {
protected:
    string cleanData(const string& data) override {
        cout << "Cleaning JSON data...\n";
        return data;
    }

    string validateData(const string& data) override {
        cout << "Validating JSON data...\n";
        return data;
    }

    string transformData(const string& data) override {
        cout << "Transforming JSON data...\n";
        return data;
    }

    void saveData(const string& data) override {
        cout << "Saving JSON data...\n";
    }
};

// Example usage
int main() {
    CSVDataProcessor csvProcessor;
    JSONDataProcessor jsonProcessor;

    cout << "---- Processing CSV ----\n";
    csvProcessor.processData("CSV sample data");

    cout << "\n---- Processing JSON ----\n";
    jsonProcessor.processData("JSON sample data");
}

3. Plugin Architecture

cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <algorithm>
using namespace std;

// Plugin interface
class Plugin {
public:
    virtual string getName() const = 0;
    virtual string getVersion() const = 0;
    virtual void execute() const = 0;
    virtual bool isEnabled() const = 0;
    virtual ~Plugin() = default;
};

// Plugin Manager
class PluginManager {
    vector<unique_ptr<Plugin>> plugins;

public:
    void registerPlugin(unique_ptr<Plugin> plugin) {
        cout << "Registered plugin: " << plugin->getName() << endl;
        plugins.push_back(move(plugin));
    }

    void executePlugins() const {
        for (const auto& plugin : plugins) {
            if (plugin->isEnabled()) {
                plugin->execute();
            }
        }
    }

    vector<const Plugin*> getPlugins() const {
        vector<const Plugin*> list;
        for (const auto& p : plugins) list.push_back(p.get());
        return list;
    }
};

// ✅ NEW: Backup Plugin (no modification to manager)
class BackupPlugin : public Plugin {
public:
    string getName() const override { return "Backup Plugin"; }
    string getVersion() const override { return "1.0"; }
    bool isEnabled() const override { return true; }
    void execute() const override { cout << "Running backup..." << endl; }
};

// ✅ NEW: Security Scan Plugin (no modification to manager)
class SecurityScanPlugin : public Plugin {
public:
    string getName() const override { return "Security Scanner"; }
    string getVersion() const override { return "2.1"; }
    bool isEnabled() const override { return true; }
    void execute() const override { cout << "Running security scan..." << endl; }
};

// Example usage
int main() {
    PluginManager manager;

    manager.registerPlugin(make_unique<BackupPlugin>());
    manager.registerPlugin(make_unique<SecurityScanPlugin>());

    cout << "\nExecuting enabled plugins:\n";
    manager.executePlugins();

    cout << "\nLoaded Plugins:\n";
    for (const Plugin* p : manager.getPlugins()) {
        cout << "- " << p->getName() << " v" << p->getVersion() << endl;
    }

    return 0;
}

🔍 How to Identify OCP Violations

Questions to Ask

  1. Do I need to modify existing classes when adding new features?
  2. Are there multiple if/else or switch statements based on types?
  3. Is the class tightly coupled to specific implementations?
  4. Do I need to recompile and retest existing code for new features?

Warning Signs

  • ❌ Long if/else chains or switch statements
  • ❌ Frequent modifications to the same classes
  • ❌ Hard-coded dependencies
  • ❌ Classes that need to "know" about all possible implementations
  • ❌ Difficulty adding new features without extensive changes

🎯 Benefits of Following OCP

1. Stability

cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;

// LogHandler interface
class LogHandler {
public:
    virtual void handle(const string& message) const = 0;
    virtual ~LogHandler() = default;
};

// ✅ Existing Logger class — open for extension, closed for modification
class Logger {
    vector<unique_ptr<LogHandler>> handlers;

public:
    void addHandler(unique_ptr<LogHandler> handler) {
        handlers.push_back(move(handler));  // No modification to add new handlers
    }

    void log(const string& message) const {
        for (const auto& handler : handlers) {
            handler->handle(message);
        }
    }
};

// ✅ NEW: Console log handler
class ConsoleLogHandler : public LogHandler {
public:
    void handle(const string& message) const override {
        cout << "[Console] " << message << endl;
    }
};

// ✅ NEW: File log handler
class FileLogHandler : public LogHandler {
public:
    void handle(const string& message) const override {
        // Example simulation (in real-world, you'd write to a file)
        cout << "[File] Logging message to file: " << message << endl;
    }
};

// ✅ NEW: Slack log handler — no modification to Logger class
class SlackLogHandler : public LogHandler {
public:
    void handle(const string& message) const override {
        // Simulate sending message to Slack
        cout << "[Slack] Sending to Slack channel: " << message << endl;
    }
};

// Example usage
int main() {
    Logger logger;

    logger.addHandler(make_unique<ConsoleLogHandler>());
    logger.addHandler(make_unique<FileLogHandler>());
    logger.addHandler(make_unique<SlackLogHandler>()); // Added new handler without changing Logger

    logger.log("System started successfully!");
    logger.log("User logged in!");
}

2. Easier Testing

java
@Test
public void testNewDiscountStrategy() {
    // Test only the new strategy, existing code unaffected
    DiscountStrategy strategy = new StudentDiscountStrategy();
    double discount = strategy.calculateDiscount(100.0);
    assertEquals(15.0, discount, 0.01);
}

3. Parallel Development

java
// Team A can work on new payment methods
public class ApplePayPayment implements PaymentStrategy {
    // Implementation
}

// Team B can work on new shipping methods
public class DroneShipping implements ShippingStrategy {
    // Implementation
}

// Neither team affects the other's work

🚨 Common OCP Mistakes

❌ Over-Engineering

Don't create abstractions prematurely:

java
// ❌ BAD: Creating unnecessary abstraction
public interface NumberProcessor {
    int process(int number);
}

public class AddOneProcessor implements NumberProcessor {
    public int process(int number) { return number + 1; }
}

// ✅ BETTER: Simple method until extension is actually needed
public class Calculator {
    public int addOne(int number) { return number + 1; }
}

❌ Wrong Abstraction Level

java
// ❌ BAD: Too specific interface
public interface DatabaseUserRepository {
    void saveUserToMySQL(User user);
    void saveUserToPostgreSQL(User user);
}

// ✅ BETTER: Proper abstraction level
public interface UserRepository {
    void save(User user);
    User findById(Long id);
}

🛠️ Refactoring to OCP

Step-by-Step Process

  1. Identify Variation Points: Find code that changes frequently
  2. Extract Abstractions: Create interfaces or abstract classes
  3. Implement Strategies: Create concrete implementations
  4. Use Dependency Injection: Inject dependencies instead of creating them
  5. Register/Configure: Use configuration or registration mechanisms

Example Refactoring

cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Data {
    string content;
};

// ❌ Violation: New formats require modifying this class
class ReportGenerator {
public:
    void generateReport(const string& type, const vector<Data>& data) {
        if (type == "PDF") {
            cout << "Generating PDF report..." << endl;
            // Generate PDF logic
        } else if (type == "Excel") {
            cout << "Generating Excel report..." << endl;
            // Generate Excel logic
        }
        // ❌ Adding CSV or HTML would require editing this class again
    }
};

int main() {
    ReportGenerator generator;
    vector<Data> dataset = {{"Sample Data 1"}, {"Sample Data 2"}};
    generator.generateReport("PDF", dataset);
    generator.generateReport("Excel", dataset);
}

🎓 Practice Exercise

Exercise: Refactor the Notification System

Here's a system that violates OCP. Can you refactor it?

cpp
#include <iostream>
#include <string>
#include <regex>
using namespace std;

class NotificationService {
public:
    void sendNotification(const string& type, const string& message, const string& recipient) {
        if (type == "EMAIL") {
            // Send email
            cout << "Email sent to " << recipient << ": " << message << endl;
        } else if (type == "SMS") {
            // Send SMS
            cout << "SMS sent to " << recipient << ": " << message << endl;
        } else if (type == "PUSH") {
            // Send push notification
            cout << "Push notification sent to " << recipient << ": " << message << endl;
        }
    }

    bool isValidRecipient(const string& type, const string& recipient) {
        if (type == "EMAIL") {
            return recipient.find('@') != string::npos;
        } else if (type == "SMS") {
            // Check if it's exactly 10 digits
            return regex_match(recipient, regex("\\d{10}"));
        } else if (type == "PUSH") {
            return !recipient.empty();
        }
        return false;
    }
};

int main() {
    NotificationService notifier;

    string email = "rkroy@example.com";
    string phone = "9876543210";
    string device = "UserDevice123";

    if (notifier.isValidRecipient("EMAIL", email))
        notifier.sendNotification("EMAIL", "Welcome to our platform!", email);

    if (notifier.isValidRecipient("SMS", phone))
        notifier.sendNotification("SMS", "Your OTP is 1234", phone);

    if (notifier.isValidRecipient("PUSH", device))
        notifier.sendNotification("PUSH", "You have a new message!", device);
}

Solution Approach

  1. Create NotificationChannel interface
  2. Implement EmailNotification, SMSNotification, PushNotification
  3. Create NotificationService that uses strategies
  4. Add registration mechanism for new notification types

📚 Summary

The Open/Closed Principle is about designing software that can grow and evolve without breaking existing functionality. By following OCP:

  • Stability: Existing code remains unchanged and stable
  • Extensibility: New features can be added easily
  • Maintainability: Reduced risk of introducing bugs
  • Testability: New features can be tested in isolation
  • Flexibility: System adapts to changing requirements

Remember: Open for Extension, Closed for Modification!

The key is to identify the parts of your system that are likely to change and abstract them behind stable interfaces. Use design patterns like Strategy, Template Method, and Plugin architectures to achieve OCP compliance.