Design Pattern Variations: A Better Visitor
- A Problem Visitor
- A Better Visitor
- Solving D1, D2, and D3
- Download Sample Program
- Notes and References
The starting point of this article was an extremely interesting critique [1] of the Visitor Pattern (VP), reiterating its disadvantages and questioning its value as a pattern in a very exhaustive manner. As usual, this kind of thorough analysis proves to be a fertile ground for new ideas—this article will present a couple of variations of the pattern responding systematically to all the major shortcomings of VP.
A Problem Visitor
The following is a short description of the original Visitor Pattern (VP), emphasizing its mechanics and the way in which it affects its perceived usability. We will use the example provided in the aforementioned article to illustrate.
VP is often defined as "a way of separating an algorithm from an object structure upon which it operates." This description implies the existence of three main collaborating parts:
- An algorithm (ALG)
- A structure of objects (SO)
- A way of traversing 2 to apply 1 (TRAV)[1]
The alert reader will observe immediately a similitude with STL and the way it separates data from algorithms. The obvious benefit is that we can freely vary the algorithms working on top of the same data. The difference is that SO is a structure of unrelated objects[2] that can be independently inspected during traversal, whereas in STL we deal with collections of homogeneous components.
Let's consider the following standard example ([1]):
class Hammer; class Drill; class Visitor { public: void visit(Hammer & h) = 0; void visit(Drill & d) = 0; }; // root of the given hierarchy class Tool { public: virtual void accept(Visitor & v) = 0; // regular operations of Tool omitted }; class Hammer : public Tool { public: virtual void accept(Visitor & v) { v.visit(*this); } // regular operations of Hammer omitted }; class Drill : public Tool { public: virtual void accept(Visitor & v) { v.visit(*this); } // regular operations of Drill omitted }; class DoSomethingVisitor : public Visitor { public: void visit(Hammer & h) { // do something with the hammer } void visit(Drill & d) { // do something with the drill } }; vector<Tool *> myToolBox; // filled with lots of tools void doSomethingWithAllTools() { DoSomethingVisitor v; for (size_t i = 0; i != myToolBox.size(); ++i) { Tool & t = *(myToolBox[i]); t.accept(v); } }
Observations:
- Visitor is the family of algorithms ALG that can be applied to an unrelated structure of objects SO(Hammer, Saw)
- In order to traverse SO, VP requires all elements of SO to implement an artificial interface—an acceptor (Tool, in this case). This allows SO to behave polymorphically and be iterated (vector<Tool *> myToolBox). This interface is not necessary otherwise, and its implementation is mechanical.
Disadvantages[3]:
- VP is intrusive—Hammer, Drill cannot participate in VP without implementing the Tool interface or, more generally, an arbitrary class is not a candidate for VP without having implemented an acceptor.
- VP breaks the Open-Closed Principle (Software entities should be open for extension but closed for modification). We cannot add another tool (Saw, for example) without breaking the existing Visitor interface.
- SO and ALG exist separately (Visitor::drill is not part of the Drill class as sound OO design recommends). This is not always a liability as proved by [2].
- ALG can access only SO public interfaces and doesn't have access to its internals—contrast this with ALG being part of SO (a Drill::drill has access to all Drill's internals). This comes against VP's raison d’être and is considered out of scope for this discussion.
D1 and D2 are major shortcomings, whereas D3 and D4 come at odds with the VP declared intent of adding new algorithms to existing classes, but should still be mentioned because they resonate with a more purist OO point of view.