How does the Visitor Pattern not violate the Open/Closed Principle?
A pattern is applicable to certain cases. From the GoF book (p. 333):
Use the Visitor pattern when
[...]
the classes defining the object structure rarely change, but you often want to define new operations over the structure. Changing the object structure classes requires redefining the interface to all visitors, which is potentially costly. If the object structure classes change often, then it's probably better to define the operations in those classes.
If you frequently change the classes of the objects that make up the structure, the Visitor class hierarchy can be hard to maintain. In such a case, it may be easier to define operations on the classes that make up the structure.
John Vlissides, one of the GoF, wrote an excellent chapter on the subject in his Patterns Hatching book. He discusses the very concern that extending the hierarchy is incompatible with maintaining the visitor intact. His solution is a hybrid between a visitor and an enum
-based (or a type-based) approach, where a visitor is provided with a visitOther
method called by all classes outside the "base" hierarchy that the visitor understands out of the box. This method provides you an escape way to treat the classes added to the hierarchy after the visitor has been finalized.
abstract class Visitable {
void accept(Visitor v);
}
class VisitableSubclassA extends Visitable {
void accept(Visitor v) {
v.visitA(this);
}
}
class VisitableSubclassB extends Visitable {
void accept(Visitor v) {
v.visitB(this);
}
}
interface Visitor {
// The "boilerplate" visitor
void visitB(VisitableSubclassA a);
void visitB(VisitableSubclassB b);
// The "escape clause" for all other types
void visitOther(Visitable other);
}
When you add this modification, your visitor is no longer in violation of the Open-Close Principle, because it is open to extension without the need to modify its source code.
I tried this hybrid method on several projects, and it worked reasonably fine. My main class hierarchy is defined in a separately compiled library which does not need to change. When I add new implementations of Visitable
, I modify my Visitor
implementations to expect these new classes in their visitOther
methods. Since both the visitors and the extending classes are located in the same library, this approach works very well.
P.S. There is another article called Visitor Revisited discussing precisely that question. The author concludes that one can go back to an enum
-based double dispatch, because the original Visitor Pattern does not present a significant improvement over the enum
-based dispatch. I disagree with the author, because in cases when the bulk of your inheritance hierarchy is solid, and the users are expected to provide a few implementations here and there, a hybrid approach provides significant benefits in readability; there is no point in throwing out everything because of a couple of classes that we can fit into the hierarchy with relative ease.
The first two answers are great. To expand on the observation, "A pattern is applicable to certain cases," consider OCP in two dimensions.
- Object-oriented code is open for extension when we can add new types to an existing hierarchy. We cannot add new functions to the existing types.
- Functional code is open for extension when we can add new logic to existing data structures. We cannot pass new data structures through existing functions.
This dichotomy is called the expression problem. The Visitor Pattern allows us to trade the usual dimension in which OO is extensible, and in return we gain the dimension in which FP is extensible.
To reconcile Visitor with OCP, we might say the pattern simply opens a different dimension for extensibility. A tradeoff in the dimensions of extensibility is applicable to certain cases.