The Open / Closed principle states
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification” – Wikipedia
Open to extension means that we should be able to add new behavior or features to a class and Closed for modification means that while making these changes we should not need to modify the binaries.
If we want to add new behavior that we should not need to change the existing classes or functions. We should just be able to add new classes and functions to achieve this. A simple example is that we do not need to rewire the motherboard to plug in a USB.
Now you might be thinking how can we achieve extension without making any modifications to a binary?
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 bug.
We can add seams to the applications which allows us to create the demarcation between different layers.
Approaches for achieving 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 9AM. 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 heritance 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
public class AreaCalculator { public double Area(Rectangle[] shapes) { return shapes.Sum(shape => shape.Width*shape.Height); } }
To get the area of a rectangle we will pass the object of the Rectangle class and get the area back.
public class Rectangle { public double Width { get; set; } public double Height { get; set; } }
Now we would like to extend the AreaCalculator to calculate the area of a circle as well.
public class Circle { public double Radius { get; set; } }
So we will change the AreaCalculator to something like below. So depending on the shape we can calculate the area of the shape.
public class AreaCalculator { public double Area(object[] shapes) { double area = 0; foreach (var shape in shapes) { if (shape is Rectangle) { var rectangle = (Rectangle)shape; area += rectangle.Width * rectangle.Height; } else if (shape is Circle) { var circle = (Circle)shape; area += circle.Radius * circle.Radius * Math.PI; } } return area; } }
However if tomorrow we want to extend the AreaCalculator class to include another shape then we will have to modify the class again.
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.
public abstract class Shape { public abstract double 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 individual implementation of Area and also add the properties as applicable for each shape.
public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } public override double Area() { return Width * Height; } } public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Radius * Radius * Math.PI; } }
Since each shape has its own implementation of Area so our AreaCalculator becomes much simpler and robust.
public class AreaCalculator { public double Area(Shape[] shapes) { return shapes.Sum(shape => shape.Area()); } }
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.
Find the complete source code for this post at googledrive or skydrive.
Any questions comments and feedback are most welcome.