What is the Open/Closed Principle?
Vahid Farahmandian
C# Corner MVP | Experienced .NET Developer & Architect | Proficient in ASP.NET Core, Azure DevOps and SQL Server | Eager to Relocate to USA, Norway or Germany
Let’s begin with a short summary of what the Open/Closed Principle is. It’s a principle for object oriented design first described by Bertrand Meyer that says that
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
At first thought that might sound quite academic and abstract. What it means though is that we should strive to write code that doesn’t have to be changed every time the requirements change. How we do that can differ a bit depending on the context, such as our programming language. When using Java, C# or some other statically typed language the solution often involves inheritance and polymorphism, which is what this example will illustrate.
Let’s say that we’ve got a Rectangle class. As most rectangles that I’ve encountered it has a width and a height.
public class Rectangle{
public double Width{get;set;}
publicdoubleHeight{get;set;}
}
Now our customer, wants us to build an application that can calculate the total area of a collection of rectangles. That’s not a problem for us. We learned in school that the area of a rectangle is it’s width multiplied with it’s height and we mastered the for-each-loop a long time ago.
public class AreaCalculator{
public double Area(Rectangle[] shapes){
double area =0;
foreach(var shape in shapes){
area += shape.Width*shape.Height;
}
return area;
}}
We present our solution, the AreaCalculator class to customer and he signs us his praise. But he also wonders if we couldn’t extend it so that it could calculate the area of not only rectangles but of circles as well.
That complicates things a bit but after some pondering we come up with a solution where we change our Area method to accept a collection of objects instead of the more specific Rectangle type. Then we check what type each object is of and finally cast it to it’s type and calculate it’s area using the correct algorithm for the type.
public double Area(object[] shapes){
double area =0;
foreach(var shape in shapes){
if(shape isRectangle){
Rectangle rectangle =(Rectangle) shape;
area += rectangle.Width*rectangle.Height;
}else{
Circle circle =(Circle)shape;
area += circle.Radius* circle.Radius*Math.PI;
}
}
return area;
}
The solution works and customer is happy.
Only, a week later he calls us and asks: “extending the AreaCalculator class to also calculate the area of triangles isn't very hard, is it?â€. Of course in this very basic scenario it isn't but it does require us to modify the code. That is, AreaCalculator isn't closed for modification as we need to change it in order to extend it. Or in other words: it isn't open for extension.
In a real world scenario where the code base is ten, a hundred or a thousand times larger and modifying the class means redeploying it’s assembly/package to five different servers that can be a pretty big problem. Oh, and in the real world customer would have changed the requirements five more times since you read the last sentence :-)
One way of solving this puzzle would be to create a base class for both rectangles and circles as well as any other shapes that customer can think of which defines an abstract method for calculating it’s area.
public abstract class Shape{
public abstract double Area();
}
Inheriting from Shape the Rectangle and Circle classes now looks like this:
public class Rectangle:Shape{
publicdoubleWidth{get;set;}
publicdoubleHeight{get;set;}
publicoverridedoubleArea({
returnWidth*Height;
}
}
public class Circle:Shape{
publicdoubleRadius{get;set;}
publicoverridedoubleArea(){
returnRadius*Radius*Math.PI;
}
}
As we’ve moved the responsibility of actually calculating the area away from AreaCalculator’s Area method it is now much simpler and robust as it can handle any type of Shape that we throw at it.
public double Area(Shape[] shapes){
double area =0;
foreach(var shape in shapes){
area += shape.Area();
}
return area;
}
In other words we’ve closed it for modification by opening it up for extension.
If we look back our previous example, where did we go wrong? Clearly even our first implementation of the Area wasn’t open for extension. Should it have been? I’d say that it all depends on context. If we had had very strong suspicions that customer would ask us to support other shapes later on we could probably have prepared for that from the get-go. However, often it’s not a good idea to try to anticipate changes in requirements ahead of time, as at least my psychic abilities haven’t surfaced yet and preparing for future changes can easily lead to overly complex designs. Instead, I would suggest that we focus on writing code that is well written enough so that it’s easy to change if the requirements change.
Once the requirements do change though it’s quite likely that they will change in a similar way again later on. That is, if customer asks us to support another type of shape it’s quite likely that he soon will ask for support for a third type of shape.
So, in other words, I definitely think we should have put some effort into abiding by the open/closed principle once the requirements started changing. Before that, in most cases, I would suggest limiting your efforts to ensuring that the code is well written enough so that it’s easy to refactor if the requirements starts changing.
find more at here