The Visitor Pattern
The Visitor Pattern
Contextual Forces
Motivation
From GoF: "Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates."
Many people find this difficult to understand. However, if we break this statement down into sections and explain each one, the motivation of the pattern begins to take shape:
"Represent an operation" - provide an abstraction that defines an operation to be performed, that is, a method definition or set of method definitions that are designed to perform an operation.
"...to be performed on the elements of an object structure" - This represented operation is designed to "operate on" (this is, effect the state of) a set of classes that are all part of a larger structure. Since the classes represent a variation of some sort, this representation will also have to allow for different classes to be effected in different ways.
"Visitor lets you define a new operation..." - Once this operation is defined by a representation, you can provide implementations of it.
"...without changing the classes of the elements on which it operates." - These implementations can (somehow) be added to the behavior of the system without any alteration of the existing class definitions.
Put another way: If you have a structure of classes in your product, and you expect there will be a need to add type-specific behaviors to them after the product has been deployed, sold, shipped, or otherwise committed to, you can use the Visitor Pattern to prepare them for future enhancement, without committing to the precise nature of these additional behaviors, how many of them there will be, and so forth.
Encapsulation
Usually the structure of the classes to which behavior will be added is already encapsulated, and the Visitor Pattern maintains this encapsulation. However, these other issues are also encapsulated:
- The interface that allows for the addition of new behaviors.
- The behaviors themeselves, both their implementation and their numbers.
- Which behavior is being added at any given point at runtime.
- The specific relationship between any Visited class and and Visitor class is also encapsulated.
Procedural Analog
There is, perhaps, no direct procedural analog to the Visitor Pattern, but a close candidate is the concept of a callback pointer. In procedural systems, often pointers are passed from a routine that in fact point to the routine itself, to allow the receiver of the pointer to call back upon the routine with parameters that will cause the routine to serve the callers needs. This is essentially what the Visitor Pattern does, but in a way that allows for future additions with little code maintainenance.
Non-Software Analog
The front door of your home is designed (among other things) to allow other people (not in your family) to enter for various purposes. A salesperson may want to sell a product, for instance. A relative may want to ask a favor. The next door neighbor may want to inform you of a problem in the neighborhood, or ask you to water his plants while he is on vacation. The various people in your home will respond in different ways, and have different results from the actions of the very same visitor. This is, in fact, why the Visitor Pattern is so named: it provides a mechanism for visitation, which can then be used in the future for various purposes.
Implementation Forces
Example
Pseudocode:
abstract class Visited { abstract void accept(Visitor v); } class VisitedTypeA : Visited { private int stateForA = 0; // Note typesafe call public void accept(Visitor v) { v.VisitTypeA(this); } // Note: These methods are not visible to clients who hold this reference in an upcast to Visited public int getAState(){ return stateForA; } public void setAState(int value){ stateForA = value; } } class VisitedTypeB : Visited { private double stateForB = 0.0; // Note typesafe call public void accept(Visitor v) { v.VisitTypeB(this); } // Note: These methods are not visible to clients who hold this reference in an upcast to Visited public double getBState(){ return stateForB; } public void setBState(double value){ stateForB = value; } } abstract class Visitor { abstract void visitTypeA(VisitedTypeA vt); abstract void visitTypeB(VisitedTypeB vt); } class VisitorBehavior1 : Visitor { // Note vt is an implicit downcast to the concrete type, allowing access to the full interface public void visitTypeA(VisitedTypeA vt){ int x = vt.getAState(); // Perform operation 1 on the int held by type A, set x to result vt.setAState(x); } // Note vt is an implicit downcast to the concrete type, allowing access to the full interface public void visitTypeB(VisitedTypeB vt) : Visitor{ double x = vt.getBState(); // Perform operation 1 on the double held by type B, set x to result vt.setBState(x); } } class VisitorBehavior2 { // Note vt is an implicit downcast to the concrete type, allowing access to the full interface public void visitTypeA(VisitedTypeA vt){ int x = vt.getAState(); // Perform operation 2 on the int held by type A, set x to result vt.setAState(x); } // Note vt is an implicit downcast to the concrete type, allowing access to the full interface public void visitTypeB(VisitedTypeB vt){ double x = vt.getBState(); // Perform operation 2 on the double held by type B, set x to result vt.setBState(x); } } // Client behavior // Visited someVisitedType; // Visitor someVisitor; // ...use a factory or other mechanism to fill these pointers will appropriate instances // someVisitedType.accept(someVisitor);
Questions, concerns, credibility checks
- The interface of the Vistor tends to be coupled to the types in the structure being visited. Therefore, if this structure is likely to change on its own, the Visitor implementations will incur a lot of cost in terms of maintenance. The Visitor Pattern is best used on structures that are unlikely to change (we often think of this in terms of shrink-wrap software, where "change" often means an entirely new version).
- In order for the Visited classes to remain decoupled from the specific Visitor implementations (allows new Visitor implementations to be added later, which is the point), all Visitor classes must share a common interface. Whether or not this is credible should be considered early in making the decision as to whether this approach appropriate.
- The use of virtual method calls is typical, and thus the Visitor Pattern can impede performance. However, the use of constrained generics can help ameliorate this (TODO: Generics implementation here)
Options in implementation
Consider method overloading to simplify the use of the Visitor, if it is available in your implementing language:
abstract class Visitor {
abstract void visit(VisitedTypeA vt);
abstract void visit(VisitedTypeB vt);
}
Also, it may be possible (though potentially costly) to use reflection to determine the type being visited, rather than exposing a broad interface:
abstract class Visitor { public void visit(Visited vt) { // perform reflection on vt here switch(type) { case TypeA: visitTypeA((VisitedTypeA)vt); case TypeB: visitTypeB((VisitedTypeB)vt); } } protected abstract visitTypeA(VisitedTypeA vt); protected abstract visitTypeB(VisitedTypeB vt); }
This is generally only advisible in systems where meta data helps to overcome the performance costs of reflection.
Consequent Forces
The Visitor enables a potential business relationship in shrink-wrap software, in that another software development company can produce extenstions and enhancements to your product, without the need for you to expose very much of the internal implementation details of your system. It does this at the expense of a fair amount of identity coupling to your internal types, and thus if your product is versioned, more than likely the "plug in" products that enhance it will have to be versioned as well. This often suits both parties in this business relationship.
The Visitor Pattern also can be used in testing. The "additional behavior" added to a structure of classes could be a suite of tests that take varying actions against the varying types in the structure.
Testing issues
Testing the Visitor at first would appear to be tricky, since there is coupling in both directions (from the Visited to the Visitor, given the dependency, and from the Vistor to the Visited, given the callback).
However, the use of Mock objects on both sides of the relationship can solve this problem.
To test a given Visitor, one hands it to a Mock Visited type (once for each type that can be visited), and then the mock is inspected to ensure that the Visitor called for and changed its state in the way expected for the particular Visitor being tested. If VisitorBehavior1 calls for and changes state whereas VisitorBehavior2 only calls for and changes a subset of the state, this can be reflected and verified in the inspection of the mock.
To test a given Visited class, a single mock Visitor is use to ensure that the Visited class calls the proper method at runtime. In a typesafe language this may not be necessary since the parameter type of each method can ensure this at compile time.
Cost-Benefit (gain-loss)
The benefits of the Vistor Pattern include:
- Seperation of concerns keeps the client object simpler
- Adding new Visitor implementations is very open-closed, effecting only the entity that instantiates them (argubly a factory of some kind)
- Any potential duplication among the Visitors (or the Visited) can be pushed up into the base abstract class.
The costs of the Visitor Pattern include:
- The coupling of the Visitor interface to the concrete types being visited means that added new visited types creates a lot of maintainence problems.