When you need to modify a class to change its behaviour, refactor it in such a manner that you don’t need to modify it again for the same reason.
The Open-Closed Principle (OCP) is the second of the five SOLID principles of object-oriented design. It states that:
What does this mean?
Let us first talk about classes. We know that, from time to time, as the requirements for a system change, we need to modify the code in one or more classes of the system.
The OCP suggests that we should be able to extend the behaviour of a class, without having to modify its code. How is that possible?
Let’s take an example: suppose we have a class called ‘BankLoan, which represents the behaviour of, and information about, a loan from a bank. One of its public methods is: getInterestForMonth:
[Typically, there is another public method called getInterestForPartMonth (Date startDate, Date endDate), which is used only in the first and the last months of a loan, for computing the interest for the partial month. But we can ignore that method, at least for the time being.]
Simple stuff. The code has been tested, delivered to the customer, and put into production.
Now, some time later, our customer tells us that while for the long-term loans, the bank will continue to calculate interest for a month as above (1/12th of the annual interest amount), for short-term loans, say, for tenure of less than or equal to 91 days, they have decided to compute the interest based on the actual number of days in the month. So, for example, since March has more days than April, the interest charged to a borrower in the month of March will be proportionately more than the one in April.
This suddenly complicates things. What about February and leap years? In a leap year, February has 29 days out of 366 for the year, while in other years, it has 28 days out of 365. Our customer tells us that we have to account for leap years correctly. Thus, the interest that a borrower will pay for:
- January in a leap year will be 31/366 of the full year’s interest
- January in a non-leap year will be 31/365 of the full year’s interest
- February in a leap year will be 29/366 of the full year’s interest
- February in a non-leap year will be 28/365 of the full year’s interest
It turns out, that these are just some of the ways in which banks compute interest. There are other ways too.
- Our current, simple implementation (1/12th of the annual interest for a month) is known as 30/360 in banking parlance.
- The proposed, new requirement is known as actual/actual
- And there are others, such as actual/365 fixed, in which the extra day of a leap year is ignored
(For a more detailed description of the above, and other methods of interest computation, please refer to this Wikipedia article.)
To summarize, our customer asks us to retain the 30/360 approach for long-term loans (more than 91 days), and actual/actual approach for short-term loans (less than or equal to 91 days).
We now realize that we must change the signature of the getInterestForMonth method. It will need to know the month and year for which the computation is to be done. Apart from this, the implementation will also depend on the tenure of the loan.
So a modification to the BankLoan class is inevitable. But can we use this as an opportunity to refactor the code in such a manner that if in the future, the bank asks us to incorporate some other interest computation approaches, such as actual/365 fixed, we would not have to modify the BankLoan class any further.
Essentially, what we need to do is to extract the variant part of the class (interest computation approach) away from it. One way to achieve this would be to have a separate class hierarchy, with a common interface, as shown in the diagram below:
This refactoring may have been quite some work, and many client classes may have to be modified for this. But those would have had to be modified even if we had chosen not to separate the variant part from the BankLoan class.
But now, because of this update, our BankLoan class would no longer have to be modified if in the future, our customer asks us to add another interest computation approach, such as actual/365 fixed.
The BankLoan class is now open for extending its behaviour, while being closed for modification.
Applying the Open-Closed Principle makes it easier to subsequently extend the behavior of a class without having to modify its source code. The key to achieving this is to have a separate class hierarchy with an interface, in which we extract the variant behaviour(s) of the original class.
Of course, it would also have been possible to make BankLoan an abstract class, with getInterestForMonth as an abstract method, and have separate subclasses for each of the different interest computation strategies. But as we’ll see in another article later, using delegation to a class through an interface (as in our implementation) is usually a better approach than having subclasses.
We can see the OCP approach in many of the GoF design patterns, such as Strategy, State, Abstract Factory, and Bridge. All these patterns use delegation to another class through an interface.
However, two of the GoF design patterns — Template Method and Factory Method, can be seen to use the OCP approach using concrete subclasses for extracting variant behaviour from an abstract superclass.
Though the example presented in this article applies OCP to a class, we can similarly apply the OCP to other types of software entities, such as components.