Introduction
One thing is certain in all software development, all software changes during its life cycle. So, it is imperative that developers design software that is stable (i.e. does not change) in the face of ever changing functional requirements. To aid in this development, Robert Martin gave us the open-closed principle, which states:
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION [Martin].
Software modules that satisfy the open-closed principle have two huge benefits. First, they are "open for extension". (The behavior of the modules can be altered to satisfy the changing requirements.) Second, these modules are "closed for modification". (The modules, themselves are not allowed to change.) In this article, we will describe the open closed principle via a simple example.
Background
Consider the following example. We have an application that must be able to manage different kind of shapes (i.e. circles, squares, polygon, etc.) and draw them on a standard GUI. Using techniques that do not conform to the open-closed principle, we might solve this problem as shown below:
class Shape
{
private Point _center;
#region Center Get / Set
};
class Circle : Shape
{
private int _radius;
#region Radius Get / Set
}
class Square : Shape
{
private int _side;
#region Side Get / Set
}
These first classes encapsulate the shapes' data structure. The following manager performs several operations on the shapes such as draw and serialize.
class DrawManager
{
private List<Shape> _shapeList;
public DrawManager()
public void add(Shape s)
public void serializeShape(string fileName)
public void drawShapes()
{
foreach(Shape s in _shapeList)
{
if (s is Circle)
{
_drawCirle((s as Circle));
}
else if (s is Square)
{
_drawSquare((s as Square));
}
}
}
private void _drawCirle(Circle c)
private void _drawSquare(Square sq)
private void _serializeCirle(Circle c)
private void _serializeSquare(Square sq)
};
The method drawShapes walks a list of Shape, examining the Shape type and then calling the appropriate private draw method (either _drawCircle or _drawSquare). The problem with the above design is that it violates the Open Closed Principle because it cannot be closed against new kinds of shapes. If I wanted to extend this function to be able to draw a list of shapes that included triangles, I would have to modify the function. In fact, I would have to modify the function for any new type of shape that I needed.
Of course this program is only a simple example. In real life, the switch statement in the drawShapes function would be repeated over and over again in various functions all over the application; each one doing something a little different (for example, the method serializeShape has the same problem). Adding a new shape to such an application means hunting for every place that such switch statements (or if/else chains) exist, and adding the new shape to each. Moreover, it is very unlikely that all the switch statements and if/else chains would be as nicely structured as the one in drawShapes. It is much more likely that the predicates of the if statements would be combined with logical operators, or that the case clauses of the switch statements would be combined so as to “simplify” the local decision making. Thus the problem of finding and understanding all the places where the new shape needs to be added can be non-trivial.
Solution
The following code is a possible solution to the shapes problem that it conforms to the open-closed principle. In this case, the abstract Shape class defines a single pure-virtual function called Draw. Both Circle and Square override the function in a different way.
abstract class Shape
{
private Point _center;
public abstract void Draw();
};
class Circle : Shape
{
private int _radius;
public override void Draw()
}
class Square : Shape
{
private int _side;
public override void Draw()
}
class DrawManager
{
public void drawShapes()
{
foreach (Shape s in _shapeList)
s.Draw();
}
}
Note that if we want to extend the behavior of the DrawShapes function to draw a new kind of shape, now, all we need to do is add a new derivative of the Shape class. The DrawShapes function does not need to change. Thus DrawShapes conforms to the open-closed principle. Its behavior can be extended without modifying it.
In the real world, the Shape class would have many more methods. Yet adding a new shape to the application is still quite simple since all that is required is to create the new derivative and implement all its functions. There is no need to hunt through all of the application looking for places that require changes. Since programs that conform to the open-closed principle are changed by adding new code, rather than by changing existing code, they do not experience the cascade of changes exhibited by non-conforming programs.
Points of Interest
As you could realize, the Open Closed Principle is referring to using Abstract Classes and their Concrete SubClasses to extend behavior. This essentially means using something like the Template Method Pattern or Strategy Pattern to delegate either part or all of an algorithm in a Concrete SubClass [Gof]. A Template Method is a Method in an (abstract) base class which calls one or more HookMethods to fulfill parts of its tasks. You can easily extend or change the behavior of the abstract base class by creating new subclasses that implement a new version of the Hook method. This fulfills the requirements of the Open-Closed Principle.
Conclusion
It should be clear that no significant program can be 100% closed. For example, consider what would happen to the DrawShapes function if we decided that all Circles should be drawn before any Squares. The DrawShapes function is not closed against a change like this. In general, no matter how “closed” a module is, there will always be some kind of change against which it is not closed. Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience. The experienced designer knows the users and the industry well enough to judge the probability of different kinds of changes. He then makes sure that the open-closed principle is invoked for the most probable changes.
References
History
- June 2006: Initial version