In this post, we are going to learn about the Open/Closed Principle which is one of the five SOLID design principles described by Robert C. Martin.
What is the Open/Closed Principle?
First, let’s summarize the core definition of what the Open/Closed Principle is. The Open-Closed Principle (OCP) states that software entities (classes, modules, methods, etc.) should be open for extension, but closed for modification.
In other words, with this principle, we will be able to create software entities whose behavior can be changed for a particular reason, such as a change in requirements, without the need to modify, recompile then deploy the code again. The simplest way to present this principle is to consider a software method that does a single task.
Assuming we have a method that logs exceptions to a particular text file, the name of which is hard-coded into the method itself. It is obvious that the file size will grow with time. Therefore, there is a need to change the requirements of the logging method by adding a timestamp to the filename, which will result in creating a new log file every day or every month, that means the filename should be dynamic and we must open up the method and change the code, compile then test and deploy the new assembly. To summarize, if the filename was passed in as a parameter from the beginning, we could modify the behavior of the logging method without changing its core code. That means we could open it for extension but close it to modification.
The explanation provided so far might sound quite abstract. But it should become easy to understand the moment we apply this principle in a programming language such as C# or Java. However, keep in mind that we should always try to write code that doesn’t have to be changed dramatically every time the requirements change. Next in this post, we are going to use C# along with applying inheritance and polymorphism to give a concrete example of the Open/Closed principle.
There Are No Dumb Questions
Q: Open for extension and closed for modification? That sounds very contradictory. How can a design be both?
A: That’s a very good question. It certainly sounds contradictory at first. After all, the less modifiable something is, the harder it is to extend, right? As it turns out, though, there are some clever OO techniques for allowing systems to be extended, even if we can’t change the underlying code. Think about the Observer Pattern, by adding new Observers, we can extend at any time.
Q: Okay, I understand Observable, but how do I generally design something to be extensible, yet closed for modification?
A: Many of the patterns give us time-tested designs that protect your code from being modified by supplying a means of extension. We can make use of the Decorator Pattern to follow the Open-Closed principle.
Q: How can I make every part of my design follow the Open-Closed Principle?
A: Usually, you can’t. Making OO design flexible and open to extension without the modification of existing code takes time and effort. In general, we don’t have the luxury of tying down every part of our designs (and it would probably be wasteful). Following the Open-Closed Principle usually introduces new levels of abstraction, which adds complexity to our code. You want to concentrate on those areas that are most likely to change in your designs and apply the principles there.
Q: How do I know which areas of change are more important?
A: That is partly a matter of experience in designing OO systems and also a matter of knowing the domain you are working in which can help you identifying the areas of change in your own designs.
Open/Closed Principle with C# Example
In this section, we will explore a C# example to explain the OCP. Let’s assume we have a Rectangle class that has two public properties, a width, and a height.
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}
Let's assume we are building a .NET application and one of the requirements is to draw a collection of rectangles.
That’s should be straightforward for any developer. We will have to write a method that accepts a collection of rectangles. Then, inside the method, we should do a for-each-loop to draw all the rectangles. Here is the class Drawer with the method:
public class Drawer
{
public void DrawShapes(Rectangle[] shapes)
{
foreach (var shape in shapes)
{
DrawRectangle();
}
}
}
It was simple, now assume we receive another request to extend the method DrawShapes to draw circles as well and not only rectangles. That complicates things a bit but after some thinking, we may come up with a solution where we change the method DrawShapes to accept a collection of objects instead of only rectangles. Then we check what type each object is of and finally cast it to its type and draw the shape using the appropriate algorithm for the type. Here is the code of the extended method that accepts dynamic variables and then check their type inside the for/each:
public void DrawShapes(object[] shapes)
{
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
DrawRectangle();
}
else
{
DrawCircle();
}
}
}
After testing this method, we received another request to extend the Drawer class to also draw triangles. Of course, this basic request is feasible but it does require us to modify the code again. That is, the class Drawer isn’t closed for modification as we need to change it in order to extend it, in other words: it isn’t open for extension.
In a real-world scenario where the code base is (x)times larger than our example and modifying it means redeploying its assembly/package to probably different servers that can be a pretty risky task that may put the system down for a certain amount of time during the deployment. The Open/Closed Principle might be a good solution, in this case, let's see how in the next part of the C# example.
Applying the Open/Closed Principle
One way of solving this challenging situation would be to create a base class for both rectangles and circles as well as any other shapes that we can think of which defines an abstract method for drawing its area.
public abstract class Drawer
{
public abstract void DrawShape();
}
Inheriting from Shape the Rectangle and Circle classes now looks like this:
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override void DrawShape()
{
--the logic to draw a rectangle;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override void DrawShape()
{
--the logic to draw a circle;
}
}
We’ve moved the responsibility of drawing the shape away from the Drawer’s method, it is now part of the class that inherits the class Shape.
public void DrawShape(Shape[] shapes)
{
foreach (var shape in shapes)
{
shape.DrawShape();
}
}
In other words, we’ve closed the class Drawer for modification and opened it up for extension. The general idea of this principle is to write code in a way so that we will be able to add new functionality without changing the existing code. That prevents situations in which a change to one of the classes also requires us to adapt all depending classes.
When should we apply the Open/Closed Principle?
The main benefit of this principle is that it allows us to implement code that is loosely coupled. The implementations of an abstract class are independent of each other and don’t need to share any code.
It is recommended to start applying the OCP once the requirements started changing. Before that, in most cases, It is suggested to limit the efforts to ensure that the code is well written enough so that it’s easy to refactor if the requirements start changing.
Conclusion
In this post, we learned about the Open/Closed principle along with a C# example where we used inheritance and polymorphism. In some other cases, we may need interfaces instead of abstract classes but the OCP concept is the same. The OCP should allow us to write loosely coupled software along with allowing some type of changes without the need to modify the code base, this can be achieved by adding extensions as we saw in the example.
Also read Solid Principles in C# - Explore the Open Closed Principle