Modules should be open for extension, but closed for modification.
A module can be a system, collaboration, or class.
At first OCP sounds paradoxical. How can a module be open and closed?
Recall that a collaboration is a group of classes that work together to achieve a common goal. How can the collaboration accommodate an expanding goal without requiring modification?
A GMS allows users to create and draw simple shapes such as rectangles and circles as well as composite shapes such as a car:
To allow composite shapes to be nested inside of other composite shapes, we use the Composite Design Pattern as our starting point:
Here is an initial implementation:
class Shape { }
class Rectangle extends Shape {
void draw() {
System.out.println("drawing a
rectangle");
}
}
class Circle extends Shape {
void draw() {
System.out.println("drawing a
circle");
}
}
class CompositeShape extends Shape {
private static final int CAP = 100;
private Shape[] components = new
Shape[CAP];
private int size = 0; // = #components
= next vacancy
void add(Shape s) {
if (size < CAP)
components[size++] = s;
}
void draw() {
for(int i = 0; i < size; i++) {
Shape next = components[i];
// draw next
}
}
}
In the heady days of Pascal and Structured Programming, the programmer's job was to create elaborate control structures to match equally elaborate data structures. Pascal provided a toolbox for composing primitive control structures such as iterations and selections. Following that style, we might implement draw() using a multi-way selection nested inside an iteration:
void draw() {
for(int i = 0; i < size; i++) {
Shape next = components[i];
if (next instanceof Circle)
((Circle)next).draw();
else if (next instanceof
Rectangle)
((Rectangle) next).draw();
else if (next instanceof
CompositeShape)
((CompositeShape)next).draw();
else
System.err.println("unrecognized shape");
}
}
Let's consider what happens when our collaboration needs to accommodate a new Triangle shape. Of course we need to add the new class to our collaboration:
class Triangle extends Shape {
void draw() {
System.out.println("drawing a
triangle");
}
}
Unfortunately, we will also need to modify the composite shape draw method by adding a new branch:
else if (next instanceof Triangle)
((Triangle) next).draw();
Why is this problematic?
1. The general shape of our collaboration is this:
Where Control is a module that controls or manages a bunch of components. Control is the client of the Component hierarchy. One question that arises is this: If the control is large, perhaps a package consisting of many classes, then how many multi-way conditionals like the one above need to be modified? 100? What if we only remember to modify 99?
2. What if the hierarchy and the control are developed by different companies? How enthusiastic will the control developers be when they hear that new components have been added to the component hierarchy? Will the hierarchy developers send a memo telling the control developers to be sure to modify all of their multi-way conditionals to account for the new components? Perhaps if it's an important customer they will send along a rookie programmer to dive into the control code and make the changes for them.
3. Generally speaking, something like a control mechanism will be the most critical part of an system or collaboration. This is often where the system's "intelligence" is located. The components merely add functionality to the system. It might be possible for the system to still function, even if components were missing. For example, our GMS might still be usable without the Triangle class, but without the CompositeShape class we will be confined to simple shapes. It seems risky to be forced to modify the control each time a new component is added to the system. It's like requiring brain surgery just to get a haircut.
Polymorphism can be reduced to two fundamental rules:
The Subsumption Rule: A reference to a subtype can be used in any context where a reference to a super type is expected.
For example, consider the following code:
CompositeShape car = new CompositeShape();
car.add(new Circle()); // tire 1
car.add(new Circle()); // tire 2
car.add(new Rectangle()); // chasis
// etc.
Of course the add method for composite shapes has a Shape parameter. It expects a shape as an argument, but we are passing circles and rectangles as arguments. Thanks to the subsumption rule, this is no problem.
The Dynamic Dispatch Rule: Objects decide how to handle the messages they receive.
This sounds obvious. Who or what else would make this decision? Well, the programmer or the compiler might want to make this decision. In effect, the casts in the implementation of draw can be interpreted as the programmer telling the compiler exactly which methods should be invoked when the next shape in the array is to be drawn.
We can take advantage of dynamic dispatch by adding a draw method to the Shape base class.
class Shape {
void draw() {
// ???
}
}
But how should we implement this method? How can we draw a shape when we don't know what kind of shape it is? The problem is that "shape" is an abstract concept. Fortunately, most object-oriented languages allow us to create abstract classes:
abstract class Shape {
abstract void draw();
}
Now we can re-implement the CompositeShape draw method without a multi-way selection:
void draw() {
for(int i = 0; i < size; i++) {
Shape next = components[i];
next.draw();
}
}
Note that when we add Triangle to the Shape hierarchy, this code will not need to be changed. This style of programming is sometimes called Data-Driven: data (i.e., objects) not programmers should determine the flow of control. (This is also related to Dynamic Programming.)
Each class in the Shape hierarchy is equipped with a dispatch table that associates method signatures with method implementations. When the object referenced by next receives the draw message it consults the dispatch table of its class for the corresponding message handler to call. Of course rectangles search the dispatch table associated with the Rectangle class, while circles search the dispatch table associated with the Circle class. The Rectangle dispatch table associates the draw message with the Rectangle.draw method, while the Circle dispatch table associates the same message with the Circle.draw method. Thus, rectangles respond differently that circles to the draw message.
Of course the table look up requires a few extra nanoseconds. If we were certain no new shapes would ever be added to the Shape hierarchy, we might save these nanoseconds by following the Control-Driven style. But so few things in life are certain.
For a system, OCP is realized by the Open Systems Architecture and the Microkernel Architecture.
Open Systems Architecture (OSA) is also called Service-Oriented Architecture (SOA). The idea is to invite third-party developers to create components or plug-ins that users can add to their systems without needing to modify the system in any way. Having the ability to easily extend the functionality of their system makes the system more appealing in the first place.
Of course we don't want to necessarily give our source code to third-party developers. Instead, one or more interfaces for third-party components is published. Any component that implements one of these interfaces can be plugged into the system:
A reflective system takes OSA one step further by allowing the system to dynamically discover the interfaces implemented by third party components. Each component is expected to implement a meta-interface that describes the application-specific interfaces that it implements:
For a class extension means something like overriding methods, i.e. changing implementation, without changing type.
OCP': It should be possible to change the implementation of an object without modifying its class.
Strategy Pattern, State Pattern, and Bridge Pattern might be examples of this. The general idea is to encapsulate the implementation details (fields, method algorithms, platform specifics, etc.) in a separate object. The actual class methods simply delegate to this object:
Dynamic dispatch might be an example of this.
There is also a relationship to the Dynamic Dispatch Rule:
Objects should decide how to handle the messages they receive.
In particular, two objects of the same type or class might respond differently.
Note that there is a relationship here to the Abstraction Principle:
The interface of a module should be independent of its implementation
In particular, OCP implies the implementation can be changed.