Open Closed Principle
Object Oriented Design
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)
// Serialize all registered shapes
// This method is not conformed to the Open Closed Principle
public void serializeShape(string fileName)
// Draw all registered shapes
public void drawShapes()
{
// This method is not conformed to the Open Closed Principle
foreach(Shape s in _shapeList)
{
if (s is Circle)
{
_drawCirle((s as Circle));
}
else if (s is Square)
{
_drawSquare((s as Square));
}
}
}
// The concrete implementations of these methods are not
// important in this discussion
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;
// Define an abstract way to draw a shape
public abstract void Draw();
};
class Circle : Shape
{
private int _radius;
// Override the abstract method to draw a circle
// The concrete implementation is not important in this discussion
public override void Draw()
}
class Square : Shape
{
private int _side;
// Override the abstract method to draw a square
// The concrete implementation is not important in this discussion
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
- Robert C. Martin - http://www.objectmentor.com/
History
- June 2006: Initial version