Open-Closed Principle

The open-closed principle (OCP) is stated and discussed as one of the fundamental object-oriented principles by Bertrand Mayer Open closed principle states...

The open-closed principle (OCP) is stated and discussed as one of the fundamental object-oriented principles by Bertrand Mayer. Open closed principle states that ‘component should be open to extension but closed to modification.’ This means that when requirements change, you extend the behavior of such an object by adding new code, but not by modifying existing code. Here components can be classesfunctionspackages, etc.

Related definitions of open close principle

Extension: Extending the behavior of a module.

Modification: Changing the code of a module. In large complex systems, changes occur often due to ‘changing requirements,’ ‘latent errors,’ ‘performance issues,’ etc.

Open for extension: As the requirements of the application change, We can extend the module with new behavior to meet the needs of the new requirement. We change what the module does.

Closed for modification: Changes in behavior do not result in changes in the module’s source or binary code.

The open-closed principle says that components should never change, only be extended to meet changing requirements.

Benefits of the open-closed principle

Conformance to this principle is what yields the greatest benefits claimed for object-oriented technology. i.e., reusability and maintainability

  • Sometimes, modifying code to add functionalities might feel like the easy way out, but when your codebase grows, you quickly realize that adding functionality becomes harder. You are now stuck with fragile code.
  • If the module is already delivered to the customers, a change will not be accepted. If you need to change something, hopefully, you opened your module for the extension.
  • If the module is a third-party library and availably only as binary code, then if you need to change something, hopefully, the third-party opened the module for an extension.
  • Not changing existing code for the sake of implementing extensions enables incremental compilation, testing, and debugging.

Exceptions to the Open-Closed Principle

It is very difficult to build components that don’t change

  • When fixing existing defects in the module, we cannot fix the incorrect operation by extension. The component itself must be fixed.
  • When performance needs are not met, we are forced to change the implementation to perform better, usually by changing computational algorithms or data structure.

Although Bertrand Meyer suggested that we use inheritance to achieve the goal of the open-closed principle, it introduces tight coupling if the subclass depends upon the implementation details of the superclass.

Enter interfaces. They introduce an additional level of abstraction, which enables loose coupling. We can now define different implementations of the interface methods, which can be easily substituted without changing the code that uses said methods.

The implementations of an interface are independent of each other and don’t need to share any code. 

Example of open-closed principle

Let us assume you are implementing a payment module and customers are placed in either of three categories

  • Bronze customers: Get a 4% discount
  • Silver customers: Get a 6% discount
  • Gold customers: Get an 8% discount

The naive implementation of this problem would look like this

package com.logicalconstant;                                   

public class SwitchCase {
    public static double calculatePrice(CustomerType type,
double totalAmount, double shipping){
        double price = 0;
        switch (type){
            case goldCustomer:
                price = (totalAmount*0.90)+shipping;
                break;
            case silverCustomer:
                price = (totalAmount*0.95)+shipping;
                break;
            case bronzeCustomer:
                price = (totalAmount*0.98)+shipping;
        }
        return price;
    }
}

There is a serious design problem in the above example. In case we add one more ‘customer category,’ we are forced to modify the function above. Modifying existing code to accommodate new requirements may break any client using that code and may require recompilation and retesting.

As an alternative design, as shown below, the customer module is closed against any changes to the customer categories.

open closed principle

In the above design, we created the interface ‘CustomerPriceStrategy’ that would the only point of contact for any client (in this case, the ‘Customer’ class). This will give us a starting point for abstracting away the logic and separating them.

the interface for calculating the price adheres to the open-closed principle. It is possible to add or remove customer categories without modifying the existing calculation algorithms or the customer itself.

The customer class directly interacts with the interface ‘CustomerPriceStrategy’. 

  • Whenever we add new customer categories, we add a new concrete class that implements the ‘CustomerPriceStrategy’ interface rather than changing the existing classes, either customer class or existing customer categoriesclasses. For example, tomorrow, if we would like to add ‘DiamondPriceStrategy,’ then we can add one more concrete class, ‘DiamondPriceStrategy,’ and implement the interface ‘CustomerPriceStrategy’, rather than changing any existing classes.
  • Changes to derived classes which (‘GoldPriceStrategy’, ‘SilverPriceStrategy’, ‘BronzePriceStrategy’ …) implement the interface ‘SilverPriceStrategy’ will not break any client code and may not even require recompilation of some clients.
  • What we can’t do is change the interface definition. Any change here may force changes on most or all of its clients.
  • Interfaces directly support the ‘open closed’ principle. They must be extended but are closed to modification. Since they have no implementation, they have no latent errors to fix and no performance issues.

Well-designed code can be extended without modification. This means New features are added by adding new code rather than changing already existing code.

Software that is designed to be reusable, maintainable, and robust must be extensible without requiring change.

  • We do this with interfaces, abstract classes, etc.
  • They are extended by derived classes that implement the functions in different ways.

For implementing the above example, A customer can get a discount, but there is three logic to calculate the discount based on the type of customer, i.e., the bronze customer gets a 4% discount,  Silver customers get a 6% discount, the gold customer gets 8% discount.

First, create an interface.

public interface CustomerPriceStrategy {                        
    double calculatePrice ();
}

Now time to implement the price logic

BronzePriceStrategy.Java

public class BronzePaymentStrategy implements CustomerPriceStrategy {
    @override
    public double price (double amount) {
        
        return amount * 0.04; 
    }
}

SilverPriceStrategy.Java

public class SilverPaymentStrategy implements CustomerPriceStrategy {
    @override
    public double price (double amount) {
        
        return amount * 0.06; 
    }
}

GoldPriceStrategy.Java

public class GoldPaymentStrategy implements CustomerPriceStrategy {
    @override
    public double price (double amount) {
        
        return amount * 0.06; 
    }
}

Suppose you now wanted to add another functionality (price strategy) to this module. Say, ‘DiamondPriceStrategy’ you can now do so by adding a new class DiamondPriceStrategy.java without modifying the addition code.

Here is a dummy class showing how the caller invokes the code above.

public class Main {

    public static void main(String[] args) {
	 CustomerPriceStrategy strategy = new GoldPriceStrategy();
     double a = strategy.calculatePrice(1200);
        System.out.println(a);
        strategy = new SilverPriceStrategy();
        a = strategy.calculatePrice(122);
        System.out.println(a);
    }
}

The above design abstract away all those if statements into independent separate classes ready for unit testing. As it is shown, our ‘CustomerPriceStrategy’ class is now open to extension as we can add endless ‘PriceStrategy’ classes and closed to modifications as we won’t need to make any changes in the class regardless of the number of ‘PriceStrategy’ classes that might be added.

Related Articles