The Open-Close principle (OCP) is the O in the well known SOLID acronym.
Bertrand Meyer is generally credited for having originated the term open/closed principle, which appeared in his 1988 book Object Oriented Software Construction. Its original definition is
- A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
- A module will be said to be closed if it is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding)
Upon these definitions the principle is usually expressed and summarized this way:
Modules should be open for extension and closed for modification.
There have been (and still are) a lot of debates about the meaning of this simple definition and its implication on the usage of Object-Oriented Programming (OOP) in the real-world.
In this post I’ll try to be as concrete and concise as possible.
The classical code example to explain OCP
The classical code example to explain OCP can be translated in C# this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Circle { } public class Square { } public static class Drawer { public static void DrawShapes(IEnumerable<object> shapes) { foreach (object shape in shapes) { if (shape is Circle) { DrawCircle(shape as Circle); } else if (shape is Square) { DrawSquare(shape as Square); } } } private static void DrawCircle(Circle circle) { /*Draw circle*/ } private static void DrawSquare(Square square) { /*Draw Square*/ } } |
I find this example a bit gross. I believe one must have no idea of what OOP is to write such code. It can obviously be refactored to something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public interface IShape { void Draw(); } public class Circle : IShape { public void Draw() { /*Draw circle*/ }} public class Square : IShape { public void Draw() { /*Draw Square*/ } } public static class Drawer { public static void DrawShapes(IEnumerable<IShape> shapes) { foreach (IShape shape in shapes) { shape.Draw(); } } } |
Let’s look at the implication of this refactoring:
- We introduced a new abstraction IShape to represent a concept that we already had in mind. Indeed, in the first code sample the method DrawShapes() already accepted a sequence of shapes. With the new IShape abstraction our design is now open to accept more shapes like Triangle or Pentagone.
- The method DrawShapes() will draw any new shape without any need for modification. In other words the DrawShapes() method implementation is closed.
Here is how the OCP is usually presented. It is all about anticipating the future changes in the system in a way that:
- When a change occurs existing code is left untouched. Existing code here is DrawShapes() concrete method body, IShape interface and Circle and Square classes.
- When a change occurs new code to implement changes is written in new classes that implement existing abstraction. New classes here could be Triangle and Pentagone.
The Point of Variation principle
Another way to see the OCP is the Point of Variation (PV) principle that states:
Identify points of predicted variation and create a stable interface around them.
I find PV more understandable than OCP because it is actionable. First identify potential variations, second build the proper abstractions around these variations.
The real challenge: Anticipation
But the real challenge is anticipation, anticipating is hard. If anticipation was easy we’d be all billionaire in bitcoins. In real world, when you anticipate the risk is high that:
- We do anticipate variations that won’t vary. This is what the YAGNI principle says (You aren’t gonna need it): Always implement things when you actually need them, never when you just foresee that you need them. Developing and maintaining an abstraction has a cost and if we won’t need it this cost is negative.
- The risk is also high that we don’t anticipate the variation that will really be needed. But once the need for variation becomes real this is your developer responsibility to refactor and create the right abstractions and the right stable code that will act upon these abstractions. This is the fool me once, don’t fool me twice idea: I am not supposed to foresee what I’ll need but I am supposed to identify and then write the right abstraction when I need it.
OCP in the real world
Here is a pragmatic approach to OCP:
- In any case the KISS principle applies (Keep It Simple Stupid): don’t underestimate the difficulty of anticipating and don’t waste your resources creating abstractions you won’t need.
- Write automatic tests: one of the greatest benefit of writing tests is that for a while, you must look at your code from the client perspective. If your code contains some area difficult to cover by tests, it certainly means that your code should be refactored to be easily 100% testable. Experience shows that when refactoring from poorly testable code to fully testable code, the need for right abstractions naturally pops up.
- Some static analyzers can help you pinpoint typical OCP violations:
- when downcasting reference (i.e casting from a base class or interface to a subclass or leaf class.),
- when using the is or as operators (as in the first example above).
- NDepend has the rule Base class should not use derivatives: matches of this rules are obvious violation of the OCP.
- Keep in mind the fool me once, don’t fool me twice idea. You must refactor code as soon as the need to abstract some concepts is identified. Of course sometime it is not possible if tons of client code depend upon your API: in this situation you cannot easily refactor and often you’ll have to live with wrong design. This is why public API design is such a sensitive topic: you have no other choice than doing your best to anticipate and to accept to live with your past design mistake.
Be open to more than one variation with the Visitor pattern
Finally let’s underline that in the real world, data objects (like the shapes here) don’t implement themselves algorithm such as drawing. Experience tells that this is a clear violation of OCP because when a new algorithm is needed on data objects, like persistence in addition of drawing for example, all shape classes must be modified again. This is also a violation of the Single Responsibility Principle (SRP, the S in SOLID) because a shape class has now two responsibilities: 1) holding the shape data 2) drawing the shape.
Hence we now have two variations: we need a way to abstract both the shapes and the algorithms applied on the shapes, in order to to write something like algorithm.ApplyOn(shape). This sort of call on two abstractions is named a double dispatching call: the implementation really invoked depends both on the IShape object’s type and the IAlgorithm object’s type. If you have N shapes and M algorithms you need [N x M] implementations.
Fortunately the visitor pattern helps implementing double dispatching. The code with the new persistence algorithm would then look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// // Shapes elements // public interface IShape { void Accept(IVisitor visitor); } public class Circle : IShape { public void Accept(IVisitor visitor) { visitor.Visit(this); } } public class Square : IShape { public void Accept(IVisitor visitor) { visitor.Visit(this); } } // // Visitors algorithms on shapes elements // don't use the IAlgorithm terminology to keep up with the classical visitor pattern terminology // public interface IVisitor { void Visit(Circle circle); void Visit(Square square); } public class DrawAlgorithm : IVisitor { public void Visit(Circle circle) { /*Draw circle*/} public void Visit(Square square) { /*Draw square*/} } public class PersistAlgorithm : IVisitor { public void Visit(Circle circle) { /*Persist circle*/} public void Visit(Square square) { /*Persist square*/} } public static class Program { public static void ApplyVisitorAlgorithmOnShapesElements(IEnumerable<IShape> shapes, IVisitor visitor) { foreach (IShape shape in shapes) { // Double dispatching: // shape can be both: Circle or Square // visitor can be both Draw or Persist shape.Accept(visitor); } } } |
Great! Especially the Visitor pattern.
About IShape interface, I think it must not know anything about draw method. It is not IShape business to know how to draw itself. it is better to get shape geometry like its points in drawer. Then drawing it.
Did you read the end of the article?
“Finally let’s underline that in the real world, data objects (like the shapes here) don’t implement themselves algorithm such as drawing. “
Sorry, I didn’t read that part of the article yet. Beautiful article, thanks.