You should be able to extend the behavior of a system without having to modify that system.
Think about that very carefully. If the behaviors of all the modules in your system could be extended, without modifying them, then you could add new features to that system without modifying any old code. The features would be added solely by writing new code.
What’s more, since none of the old code had changed, it would not need to be recompiled, and therefore it would not need to be redeployed. Adding a new feature would involve leaving the old code in place and only deploying the new code, perhaps in a new jar
or dll
or gem
.
Now you might be thinking how can we achieve extension without making any modifications to a jar
or dll
or gem
?
The answer lies in abstraction. Once we start relying on abstractions then we many ways to apply the open-close principle. In .NET we can achieve this abstraction through Interfaces and Abstract classes.
When we want to add new behavior, then we need to write new functions and new classes and this helps a single class in focus on one thing and we end up with small, easier to maintain classes. Also when we write new classes then none of the existing code is dependent on this new code and hence unlikely to introduce a bug.
We can add seams to the applications which allow us to create the demarcation between different layers.
Approaches for achieving the Open/Closed principle
Parameters
We could pass some information as parameters which could help us avoid the modifications. For example, we create a simple program to clean up temporary files on my computer at 9 AM. Now I share this code on my blog and people start using it but soon people start asking for a simple modification of allowing the users to decide the time for the cleanup to run. Now if I would have allowed this time to be a user inputted parameter then my class and function would have not needed any modifications.
Inheritance
Inheritance is another way to achieve the open-close behavior. In inheritance we allow the child classes to change and extend the behavior without making any changes to the parent classes.
Composition
To achieve Composition we could use the Strategy pattern. The strategy pattern allows us to follow the plugin model where the class doing the work gets injected into the class that needs that behavior. In this case, we have a level of abstraction between the calling code and called code. In this type of approach, the Implementation class used Inheritance since that will inherit from the base for some implementation and the client class follows composition since it exposes itself for other classes to pass into itself.
Example
Let us take the example of a class that calculates the area of a rectangle
To get the area of a rectangle we will pass the object of the Rectangle class and get the area back.
Now we would like to extend the AreaCalculator to calculate the area of a circle as well.
So we will change the AreaCalculator to something like below. So depending on the shape we can calculate the area of the shape.
However if tomorrow we want to extend the AreaCalculator class to include another shape then we will have to modify the class again.
A solution that abides by the Open/Closed Principle
Now let us try to implement the AreaCalculator class following the Open / Closed principle. Let’s start by creating an abstraction for shape. We will create a class named shape that exposes a method Area.
Now, whenever we want to create a Shape we will inherit from this abstract class. Let us now create Rectangle and Circle classes inheriting from Shape. We will provide the individual implementation of the Area and also add the properties as applicable for each shape.
Since each shape has its own implementation of Area so our AreaCalculator becomes much simpler and robust.
And since the new classes bring in their own implementations we do not need to modify the existing functionality because of the new behaviors that we add.