In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to easily follow the open/closed principle.
In essence, the visitor allows one to add new virtual functions to a family of classes without modifying the classes themselves; instead, one creates a visitor class that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.
While powerful, the visitor pattern is more limited than conventional virtual functions. It is not possible to create visitors for objects without adding a small callback method inside each class. In naive implementations, the callback method in each of the classes is not inheritable.
Contents |
A user object receives a pointer to another object which implements an algorithm. The first is designated the "element class" and the latter the "visitor class." The idea is to use a structure of element classes, each of which has an accept()
method taking a visitor
object for an argument. visitor
is a protocol (interface in Java) having a visit()
method for each element class. The accept()
method of an element class calls back the visit()
method for its class. Separate concrete visitor
classes can then be written to perform some particular operations, by implementing these operations in their respective visit()
methods.
One of these visit()
methods of a concrete visitor
can be thought of as a method not of a single class, but rather a method of a pair of classes: the concrete visitor and the particular element class. Thus the visitor pattern simulates double dispatch in a conventional single-dispatch object-oriented language such as Java, Smalltalk, and C++. For an explanation of how double dispatch differs from function overloading, see Double dispatch is more than function overloading in the double dispatch article. In the Java language, two techniques have been documented that use reflection to simplify the mechanics of double dispatch simulation in the visitor pattern: getting rid of accept() methods (the Walkabout variation), and getting rid of extra visit() methods / A time for reflection: Java 1.2's reflection capabilities eliminate burdensome accept() methods from your Visitor pattern. Despite the advantage using a reflective visitor gives of code syntax simplification it should be noted that the reflective visitor pattern may be unsuitable for iterating over a large number of visitable objects - there is a significant performance overhead of between two to fifty times per reflective method invocation compared to regular method invocation[1].
The visitor pattern also specifies how iteration occurs over the object structure. In the simplest version, where each algorithm needs to iterate in the same way, the accept()
method of a container element, in addition to calling back the visit()
method of the visitor
, also passes the visitor
object to the accept()
method of all its constituent child elements.
Because the visitor
object has one principal function (manifested in a plurality of specialized methods) and that function is called visit()
, the visitor
can be readily identified as a potential function object. Likewise, the accept()
function can be identified as a function applicator, a mapper, which knows how to traverse a particular type of object and apply a function to its elements. Lisp's object system with its multiple dispatch does not replace the Visitor pattern, but merely provides a more concise implementation of it in which the pattern all but disappears.
The following example is in the Java programming language, and shows how the contents of a tree of nodes (in this case describing the components of a car) can be printed. Instead of creating "print" methods for each subclass (Wheel, Engine, Body, and Car), a single class (CarElementPrintVisitor) performs the required printing action. Because different subclasses require slightly different actions to print properly, CarElementDoVisitor dispatches actions based on the class of the argument passed to it.
interface CarElementVisitor { void visit(Wheel wheel); void visit(Engine engine); void visit(Body body); void visit(Car car); } interface CarElement { void accept(CarElementVisitor visitor); // CarElements have to provide accept(). } class Wheel implements CarElement { private String name; public Wheel(String name) { this.name = name; } public String getName() { return this.name; } public void accept(CarElementVisitor visitor) { visitor.visit(this); } } class Engine implements CarElement { public void accept(CarElementVisitor visitor) { visitor.visit(this); } } class Body implements CarElement { public void accept(CarElementVisitor visitor) { visitor.visit(this); } } class Car implements CarElement { CarElement[] elements; public CarElement[] getElements() { return elements.clone(); // Return a copy of the array of references. } public Car() { this.elements = new CarElement[] { new Wheel("front left"), new Wheel("front right"), new Wheel("back left") , new Wheel("back right"), new Body(), new Engine() }; } public void accept(CarElementVisitor visitor) { for(CarElement element : this.getElements()) { element.accept(visitor); } visitor.visit(this); } } class CarElementPrintVisitor implements CarElementVisitor { public void visit(Wheel wheel) { System.out.println("Visiting " + wheel.getName() + " wheel"); } public void visit(Engine engine) { System.out.println("Visiting engine"); } public void visit(Body body) { System.out.println("Visiting body"); } public void visit(Car car) { System.out.println("Visiting car"); } } class CarElementDoVisitor implements CarElementVisitor { public void visit(Wheel wheel) { System.out.println("Kicking my " + wheel.getName() + " wheel"); } public void visit(Engine engine) { System.out.println("Starting my engine"); } public void visit(Body body) { System.out.println("Moving my body"); } public void visit(Car car) { System.out.println("Starting my car"); } } public class VisitorDemo { static public void main(String[] args) { Car car = new Car(); car.accept(new CarElementPrintVisitor()); car.accept(new CarElementDoVisitor()); } }
Note: A more flexible approach to this pattern is to create a wrapper class implementing the interface defining the accept method. The wrapper contains a reference pointing to the CarElement which could be initialized through the constructor. This approach avoids having to implement an interface on each element. [see article Java Tip 98 article below]
Visiting front left wheel Visiting front right wheel Visiting back left wheel Visiting back right wheel Visiting body Visiting engine Visiting car Kicking my front left wheel Kicking my front right wheel Kicking my back left wheel Kicking my back right wheel Moving my body Starting my engine Starting my car
(defclass auto () ((elements :initarg :elements))) (defclass auto-part () ((name :initarg :name :initform "<unnamed-car-part>"))) (defmethod print-object ((p auto-part) stream) (print-object (slot-value p 'name) stream)) (defclass wheel (auto-part) ()) (defclass body (auto-part) ()) (defclass engine (auto-part) ()) (defgeneric traverse (function object other-object)) (defmethod traverse (function (a auto) other-object) (with-slots (elements) a (dolist (e elements) (funcall function e other-object)))) ;; do-something visitations ;; catch all (defmethod do-something (object other-object) (format t "don't know how ~s and ~s should interact~%" object other-object)) ;; visitation involving wheel and integer (defmethod do-something ((object wheel) (other-object integer)) (format t "kicking wheel ~s ~s times~%" object other-object)) ;; visitation involving wheel and symbol (defmethod do-something ((object wheel) (other-object symbol)) (format t "kicking wheel ~s symbolically using symbol ~s~%" object other-object)) (defmethod do-something ((object engine) (other-object integer)) (format t "starting engine ~s ~s times~%" object other-object)) (defmethod do-something ((object engine) (other-object symbol)) (format t "starting engine ~s symbolically using symbol ~s~%" object other-object)) (let ((a (make-instance 'auto :elements `(,(make-instance 'wheel :name "front-left-wheel") ,(make-instance 'wheel :name "front-right-wheel") ,(make-instance 'wheel :name "rear-right-wheel") ,(make-instance 'wheel :name "rear-right-wheel") ,(make-instance 'body :name "body") ,(make-instance 'engine :name "engine"))))) ;; traverse to print elements ;; stream *standard-output* plays the role of other-object here (traverse #'print a *standard-output*) (terpri) ;; print newline ;; traverse with arbitrary context from other object (traverse #'do-something a 42) ;; traverse with arbitrary context from other object (traverse #'do-something a 'abc))
"front-left-wheel" "front-right-wheel" "rear-right-wheel" "rear-right-wheel" "body" "engine" kicking wheel "front-left-wheel" 42 times kicking wheel "front-right-wheel" 42 times kicking wheel "rear-right-wheel" 42 times kicking wheel "rear-right-wheel" 42 times don't know how "body" and 42 should interact starting engine "engine" 42 times kicking wheel "front-left-wheel" symbolically using symbol ABC kicking wheel "front-right-wheel" symbolically using symbol ABC kicking wheel "rear-right-wheel" symbolically using symbol ABC kicking wheel "rear-right-wheel" symbolically using symbol ABC don't know how "body" and ABC should interact starting engine "engine" symbolically using symbol ABC
The other-object
parameter is superfluous in traverse
. The reason is that it is possible to use an anonymous function which calls the desired target method with a lexically captured object:
(defmethod traverse (function (a auto)) ;; other-object removed (with-slots (elements) a (dolist (e elements) (funcall function e)))) ;; from here too ;; ... ;; alternative way to print-traverse (traverse (lambda (o) (print o *standard-output*)) a) ;; alternative way to do-something with ;; elements of a and integer 42 (traverse (lambda (o) (do-something o 42)) a)
Now, the multiple dispatch occurs in the call issued from the body of the anonymous function, and so traverse
is just a mapping function which distributes a function application over the elements of an object. Thus all traces of the Visitor Pattern disappear, except for the mapping function, in which there is no evidence of two objects being involved. All knowledge of there being two objects and a dispatch on their types is in the lambda function.
Aside from potentially improving separation of concerns, the visitor pattern has an additional advantage over simply calling a polymorphic method: a visitor object can have state. This is extremely useful in many cases where the action performed on the object depends on previous such actions.
An example of this is a pretty-printer in a programming language implementation (such as a compiler or interpreter). Such a pretty-printer object (implemented as a visitor, in this example), will visit nodes in a data structure that represents a parsed and processed program. The pretty-printer will then generate a textual representation of the program tree. To make the representation human-readable, the pretty-printer should properly indent program statements and expressions. The current indentation level can then be tracked by the visitor as its state, correctly applying encapsulation, whereas in a simple polymorphic method invocation, the indentation level would have to be exposed as a parameter and the caller would rely on the method implementation to use and propagate this parameter correctly.
|