The open closed principle is the second principle of the SOLID principles. It was firstly introduced by Bertrand Mayer in1988 as part of his book object oriented software construction. That principle states that, software entities should be open for extension and closed for modification.
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Bertrand Mayer – 1988
Software system evolves over time by adding new features. Adding new features should not have an impact on the existing features, and this principle aims to prevent changing existing modules/classes/packages/components when we want to add new features to the system. Hence the system should be designed in a way that allows to be extended to add new features at the same time the design should prevent any modification on the existing features.
Following this principle will reduce the risk of breaking a working functionality that is already shipped to the customer, also it will eliminate the efforts required for testing existing features got impacted because of a recent change has been added to the system.
We can achieve this target by using an abstraction such creating interfaces during your design and while adding new features you can have a new implementation for that interface rather than changing an existing class.
Another approach to achieve the same target is through inheritance, by extending the existing class and override the behavior you want to change in the subclass. That would create a new substitutable version of your existing class without touching the existing code.
The Open/Closed principle advise us to add new functionality by adding new code/class/component rather than changing existing code/class/component. Following that concept will increase system maintainability, and increase system lifetime as adding new features will not break/impact existing features. When following this role the efforts for testing will be reduced as it will be limited to the testing the newly developed features only.
Example that violates OCP principle.
Let’s assume in a banking system we need to develop a function that calculate the interest amount for customer account. The catch here the interest rate is different based on customer account type. customer can have checking account, savings account or investment account.
The below method shows bad implementation for calculating the interest rate. If new account type has been added then we will need to add one more if condition to cater for the new requirements, hence we will be changing existing code. That will need testing for all the account types however we did not change any of them we just added one new account.
public double calculateInterest() {
double interestRate = 0.0;
if (accountType.equals("savings")) {
interestRate = 0.05;
} else if (accountType.equals("checking")) {
interestRate = 0.01;
} else if (accountType.equals("investment")) {
interestRate = 0.1;
}
return balance * interestRate;
}
A better way of implementing it would be having one abstract type as an InterestCalculator that accepts argument of type Account. Interest calculator can have multiple implementations for each account type. Later when we have a new account type, we will not touch any of the existing code, we will create a new Interest calculator implementation for that new account type. Calculate Interest method will keep calling interest calculator and delegate the interest calculation operation to the interest calculator.
public double calculateInterest() {
return interestCalculator.calculateInterest(this);
}
Why it is important to follow OCP principle
- Better maintainability: a system open for extension without modifying existing code will not generate more bugs as existing code is not touched.
- Better testability: When you add new features without modifying existing code [closed for modifications] that means you do not need to test existing features as it was not impacted by the newly added features.
- Increased reusability: When a system open for extension, that means it is built in a modular way and an existing module can be replaced by a new module without touching existing code. In other words the system is built in a modular components that can be reused in different contexts.
- Scalability: Since the system is built in a modular way, it can grow from doing one feature to 100 features. Also a specific module related to a specific feature can be scaled independently if traffic increased for that specific feature.
Design for inheritance using stable interfaces
Design and document for inheritance or else prohibit it
Joshua Bloch in Effective Java
When we design a system, our system should be clear about what needs to be inherited and also we need to restrict the inheritance for the classes that we do not want our clients to inherit. Either by making them private or by making them final. In the previous example given, a new Interest calculator should implement the interface interest calculator and it should be final so nobody will come later and inherit one of the calculators instead of creating a new one. That will violate the system architecture.
Complex inheritance hierarchy could lead in difficulty in testing and maintenance. It would would be hard to change a class fifth ancestor in the inheritance hierarchy without doing major changes that might break some features. Hence inheritance should be used wisely. Complex inheritance hierarchy lead to difficult maintenance, as the inheritance tree grows over time, it will become harder to do any modification without causing more issues.
There is a known design issue called “Fragile base class” it is a known problem when a single change in one base class impact many other classes in its inheritance tree due to its complex hierarchy. Hence, While designing a system we need to design for inheritance rather than keeping it open by default. This is what is called “design for change” Identify points of predicted variations and create stable interfaces around them.
Conclusion
Software entities should be open for extension but closed for modification. I have illustrated an example of having different account types, each account will have its own calculation for the interest. That has been delegated to a strategy pattern for a better extensible implementation. An architecture has to design for inheritance or else prohibit it. to avoid overuse of inheritance as complex inheritance hierarchies are hard to test and maintain. Design patterns provide good examples on when to use inheritance or composition. System architect should identify points of predicted variations and create stable interfaces around them which known as “design for change” as software will continue to evolve.
This article is part of a series that I am writing about object modeling and Low level system design.