The Open Closed principle (OCP) is one of the most important object oriented design principle. In this article, I am going to cover what is open closed principle. How to implement this design principle in our code or module.
The open closed principle is one of the five design principles of object-oriented design. These set of five principles are known as SOLID principles.
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion
In my previous post, i have already explained single responsibility principle. In subsequent tutorials, I’ll cover rest of the principles.
Open Closed principle
The open closed principle states that the software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
The two key terms here are open for extension and closed for modification. Let’s understand the meaning of these two terms first.
Open for extension
Open for extension simply means extend the existing behaviour. We should design our classes in such a way so that behaviour or feature can be extended as the new requirement comes.
How we can extend the functionality
- By using inheritance. we create a superclass and for the different implementation we create it’s child classes which extends the functionality of a superclass.
2. By using interfaces. Here, instead of superclass we use interface that allow different implementations which we can easily substitute without changing the code.
Closed for modification
Closed for modification means once our module is developed and tested we should not modify it unless there is a bug or any change in that module. For any new requirement or feature, It should be closed for modification.
The idea behind this principle is to design our classes in such a way so that we will be able to incorporate new features/functionality without changing the existing code.
Open closed principle video tutorial
Why do we need open closed principle (OCP)
Let’s understand this through an example. Suppose, we have to design a module that processes payments from different modes (credit card, cash, gift card, etc.).
Initially, we have two sources of payments (cash and credit card). For this, we have created two classes one for accepting payment through cash and another to handle credit card payment.
CashPayment
1 2 3 4 5 6 7 8 |
package ocp; public class CashPayment { public void acceptPayment() { System.out.println("Paid through cash"); } } |
CreditCardPayment
1 2 3 4 5 6 7 8 9 |
package ocp; public class CreditCardPayment { public void acceptPayment() { System.out.println("Paid through credit card"); } } |
We also have one PaymentProcessor class which accepts payment mode. Then it invokes the method of the relevant class based on the mode of payment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package ocp; public class PaymentProcessor { public void processPayment(PaymentMode paymentMode) { if(paymentMode == PaymentMode.CASH) { CashPayment cashPayment = new CashPayment(); cashPayment.acceptPayment(); } else if (paymentMode == PaymentMode.CREDIT_CARD) { CreditCardPayment cardPayment = new CreditCardPayment(); cardPayment.acceptPayment(); } } } package ocp; public enum PaymentMode { CASH, CREDIT_CARD } |
Does this code follows the open closed principle? The answer is no. Because, let’s say in future we have to support one more payment mode for this we need to change the payment processor class.
The code in PaymentProcessor class change with every new payment mode support. This is a clear violation of the open/closed principle.
Let’s understand what’s wrong in this code.
i) It clearly impacts the code stability. In our code, we are modifying the same class when we have to incorporate new payment mode. It means we have to retest again all the payment modes. We are compromising the code stability. Any new payment mode risk the stability of previous stable code.
ii) Every new if else block increases code complexity. For every new payment mode, we are adding if else block which increases code complexity and overtime code becomes unmanageable.
How to make code extensible
We can extend the behaviour either using Inheritance or Polymorphism.
The problem with inheritance is that it introduces tight coupling, if the subclasses depend on the implementation details of their parent class.
To address the problem of inheritance, Robert C. Martin redefined the Open/Closed Principle to the Polymorphic Open/Closed Principle. The objective is to use interfaces instead of superclasses. Using interfaces we can provide different implementations without changing the existing code.
Let’s refactor our code using interface. Let’s define an interface IPay which has only one method to accept payment. All the payment mode will implement this interface.
1 2 3 4 5 6 |
package ocp1; public interface IPay { void acceptPayment(); } |
Credit card payment and cash payment implements IPay interface and defines acceptPayment method.
1 2 3 4 5 6 7 8 9 10 11 |
package ocp1; public class CreditCardPayment implements IPay { @Override public void acceptPayment() { System.out.println("Paid through credit card"); } } |
1 2 3 4 5 6 7 8 9 10 |
package ocp1; public class CashPayment implements IPay { @Override public void acceptPayment() { System.out.println("Paid through cash payment"); } } |
Similarly, When new payment mode is introduced (ex. gift card, etc.). We can implement them using separate class. New class just need to implement this interface.
1 2 3 4 5 6 7 8 9 10 11 |
package ocp1; public class GiftCardPayment implements IPay { @Override public void acceptPayment() { System.out.println("Paid through gift card"); } } |
Let’s see the PaymentProcessor class. In this processPayment() method it only accepts the reference of class which implements IPay interface and invoke it’s accept payment method.
1 2 3 4 5 6 7 8 9 10 |
package ocp1; public class PaymentProcessor { void processPayment(IPay pay) { pay.acceptPayment(); } } |
Using this approach we are not modifying the existing stable code. We are extending the behaviour without impacting the existing functionality.