A few weeks ago I did a presentation titled “How uncle Bob changed my life: An introduction to the SOLID principles” for a Swedish user group. I have previously written about a real world example of the Open/Closed Principle but during the presentation I used a much simpler example which I thought illustrated the principle quite well. I thought I’d post it here as well.
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; }
public double Height { get; set; }
}
Now our customer, Aldford (which apparently means “old river-ford”, did you know that?), 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 Aldford 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 is Rectangle)
{
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 Aldford 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 Aldford 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 Aldford 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
{
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;
}
}
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 Aldford 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 Aldford 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.
If you’ve got any other good and straight forward examples of the open/closed principle I’d love to hear about them as I really enjoy studying the SOLID principles in general and different ways to apply OCP in particular.
Any other feedback is of course also most welcome!
PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.
This blog is built with EPiServer Community, EPiServer CMS, ASP.NET MVC and a bunch of other great products. The source code is available for download at the projects page, where you also can read more about this site and my other projects.
read more
Comments
Ratn 1 years ago
Well said ,It is also good example of encapsulation .if you do good encapsulation design is mostly flexible.
Adriaan 1 years ago
This makes perfect sense, in my mind open/close principle was always classes with virtual methods. Now I know and learned something. Thanks Joel
Ed Blackburn 1 years ago
I like the example nice and simple. Personally I'd favour an interface, it helps prevent you from falling into some kind of template pattern abuse (favour composition over inheritance).
Joel Abrahamsson 1 years ago
@Ed I agree to a large degree. But I wanted to keep the example as straightforward as possible and thought the base class would best illustrate the point. I just might write a follow-up based on your feedback though :)
Nicklas 1 years ago
For pure code examples of SOLID principles you can look here: http://solidexamples.codeplex.com/
Deepa Ayachit 1 years ago
Good and simple example :)
deepa ayachit 1 years ago
But I think interface would have been better
Jasmin 11 months ago
A lot of thanks to u to illustrate the idea so nicely.It just clear up my all confusions about OCP.
Efim 9 months ago
Good example.
Question: in first example is a high coupling and low cohesion too?
thanks
Nirav 9 months ago
Joel,
Please can you put an example in interface for open/closed principle?
applicant tracking software 6 months ago
Joel, huge help. I've been working on a new project to create a base document class which holds all the shared information, while using subclasses for each specific document type. This open/close example was a huge help!
frases para orkut 2 months ago
Would it make sense to create a virtual (replaceable) the "Run" to be asking all TAG_TYPE laden objects repository so they can do what has been omitted for the default constructor as the constructor with parameters that have done?
Lig 2 months ago
Hi, I wonder if you divide the classes in projects so that you don't have to recompile the whole thing and just the dll itself would be enough, my problem has always been changing the code then you have to recompile dlls which for some reason always end up messing up the other components as well.