Skip to main content

Solid Principles

Open closed principle

O — OCP — Open-closed principle

Definitions

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.You should be able to use and add to a module without rewriting it.

This means that existing code should not be modified to add new functionality, but rather should be extended through the addition of new code. By following the OCP, code can be made more flexible and maintainable, and can avoid the introduction of bugs and errors that can occur when existing code is modified.

Why It’s Important:

  • Maintainability: Frequent changes to existing code can introduce bugs and make it harder to understand and manage.
  • Flexibility: OCP allows you to extend functionality without disrupting existing code, making it easier to adapt to new requirements.
  • Testability: Well-defined, closed modules are easier to test independently, ensuring reliability as the system evolves.

How to Achieve It:

Abstractions:

  • Use interfaces, abstract classes, or base classes to define contracts for behaviour.
  • Concrete implementations can then fulfill these contracts without modifying the core abstractions.

Dependency Injection:

  • Pass dependencies to objects through constructors or methods, rather than hardcoding them.
  • This allows you to swap implementations easily without changing the consuming code.

Design Patterns:

  • Patterns like Strategy, Template Method, and Decorator often embody OCP principles.
  • They provide flexible ways to extend behavior without modifying existing code.

Examples in object oriented programming

/***
Bad code if we want to add new payment method we need to modify processOrder function.
*/

class OrderProcessor {
    public void processOrder(Order order) {
        // ... order validation

        if (order.getPaymentMethod().equals("credit_card")) {
            processCreditCardPayment(order); // Directly handles credit card payment
        } else if (order.getPaymentMethod().equals("paypal")) {
            processPayPalPayment(order); // Directly handles PayPal payment
        } else {
            throw new UnsupportedOperationException("Unsupported payment method");
        }

        // ... order fulfillment logic
    }

    // ... methods for specific payment processing logic
}

/***
Issues:

Tightly Coupled: OrderProcessor directly handles multiple payment methods, creating strong dependencies.
Not Open for Extension: Adding new payment methods requires modifying processOrder().
Closed for Modification: Changes to payment processing logic might break existing code.
*/

How to fix this?

/***
Adhering to open closed principle.
*/

interface PaymentProcessor {
    void processPayment(Order order);
}

class CreditCardProcessor implements PaymentProcessor {
    // ... implementation for credit card processing
}

class PayPalProcessor implements PaymentProcessor {
    // ... implementation for PayPal processing
}

class OrderProcessor {
    private PaymentProcessor paymentProcessor;

    public OrderProcessor(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processOrder(Order order) {
        // ... order validation
        paymentProcessor.processPayment(order); // Delegate payment processing
        // ... order fulfillment logic
    }
}

/***
Improvements:

Abstraction: PaymentProcessor interface defines a contract for payment processing.
Dependency Injection: OrderProcessor receives the appropriate PaymentProcessor implementation, decoupling them.
Open for Extension: New payment methods can be added by implementing PaymentProcessor, without modifying OrderProcessor.
Closed for Modification: Existing OrderProcessor code remains stable even with new payment methods.
*/

More examples of code which follows open closed principle

/***
ShapeCalculator is closed for modification—it doesn't need changes to handle new shapes.
It's open for extension—new shapes can be added by implementing the Shape interface.
*/

interface Shape {
    double calculateArea();
}

class Circle implements Shape {
    // ... implementation for Circle
}

class Rectangle implements Shape {
    // ... implementation for Rectangle
}

class ShapeCalculator {
    public double calculateTotalArea(List<Shape> shapes) {
        return shapes.stream().mapToDouble(Shape::calculateArea).sum();
    }
}/***
Application is open for extension—new logging strategies can be added by implementing Logger.
It's closed for modification—no changes to Application are needed to switch logging strategies.
*/

interface Logger {
    void log(String message);
}

class ConsoleLogger implements Logger {
    // ... writes logs to the console
}

class FileLogger implements Logger {
    // ... writes logs to a file
}

class Application {
    private Logger logger;

    public Application(Logger logger) {
        this.logger = logger;
    }

    public void doSomething() {
        // ... application logic
        logger.log("Something happened!");
    }
}

How can follow open-closed principle in functional programming?

In the functional programming world, achieving this is possible using hook functions. By passing new functions, we can alter the default behavior without directly modifying the original function. Consider the saveRecord function, which takes a record as input and saves it. Now, envision future requirements for this system:

  • What if we need to perform actions before the save function?
  • What if there are tasks to execute after the save function?
  • What if changes in the logic of the save function are required?

Using hook functions enables flexibility in addressing these potential scenarios without directly altering the core functionality of saveRecord.

function saveRecord(record) {
  // code to save the record
}

Let’s write a functional programming code which follows open close principle

const saveRecord = (record, before = () => {}, after = () => {}, saveFn = defaultSave) => {
  before(); // Execute before-save logic
  saveFn(record); // Call the provided save function
  after(); // Execute after-save logic
};

const defaultSave = (record) => {
  // Default save functionality
};

/*
Open for Extension:
The saveFn argument allows for customizing the save behavior without modifying saveRecord itself.
You can pass different save implementations as needed, adhering to OCP.
Closed for Modification:
The core logic within saveRecord remains unchanged, ensuring stability and reducing risk of unintended side effects.
Default Behavior:
The defaultSave function provides a fallback for cases where no custom saveFn is provided.
*/