Using the Visitor Pattern

(211 Staff, February 1999)

Initial Caveat: This document does not try to explain the Visitor Pattern so much as clarify its usage. We presume that the reader is to some degree familiar with this design technique. To refresh a few memories, however, here is a brief summary of the pattern:

A Brief Summary of VP

We begin by assuming the following two interfaces:

   interface AcceptorI {
     void accept(VisitorI v);
   }

   interface VisitorI {
     void visitA(A a);
     void visitB(B b);
     // ...etc.  One for each subclass we need to visit
   }

(Obviously, the use of interface is not mandatory. In my experience, it is a useful technique for keeping the implementation requirements straight. There are other ways, however.)

The development of a Visitor pattern in your program now proceeds as follows:

  1. With an existing subclass/implementation of AcceptorI, we make a call to the accept() method, passing as an argument an existing subclass/implementation of VisitorI:
       AcceptorI a = (AcceptorI) acceptorCollection.getItem();
       VisitorI v1 = new DoSomethingV();
    
       a.accept(v1);
    
  2. Now for the bodies of the accept() methods. In essence, they all follow the same basic form:
       class A implements AcceptorI {
          // ...
          void accept(VisitorI v) { v.visitA(this); }
       }
    

    (As you've seen there are variants on this form: we don't always need to use the Visitee (i.e. the this pointer) in the body of a visit method. This is the most generic form, however.)

  3. Finally, the bodies of the visit method. Remember that in each case, the visit() body corresponds to some particular action which we are performing on a particular object: in other words, it's what you think of as traditional programming.
       class DoSomethingV implements VisitorI {
          void visitA(A a) { /* Do something with a */ }
          void visitA(B b) { /* Do something with b */ }
          // ...
        }
    
    

Some Subtleties

Code re-use

You have to know every possible subclass that you plan to visit(). If you add more classes to the AcceptorI hierarchy, you need to go back and modify all of your VisitorI implementations.

This is important to understand. The VP does not eliminate the problem of code modification in the face of future design expansions. What it does do, however, is to localize (and hence minimize) this modification to a very small number of places, namely the VisitorI's. Thus, we are able to re-use many of our class files, even in the face of future design changes.

Another subtlety, however: This localization presumes that there are a lot more AcceptorI's than VisitorI's in your program. If the reverse is true, then the advantages of the VP disappear. In real designs this assumption is usually safe, since the AcceptorI's typically correspond to entities in your program, while VisitorI's typically correspond to actions.

Return types of visit() and accept() Methods

It's obvious when you hear it, but maybe not so much before...

One serious limitation of the VP is that you are committed to certain function signatures. Different visit methods within the VisitorI implementation maybe have different return types, but each implementation must have exactly the same set of signatures, no matter what action the VisitorI implementation corresponds to. In the VisitorI/AcceptorI example, my implementations are forced to use only accept() and visit() methods with no return value.

The moral is: think carefully about what information (if any) you will ever need to have returned.

By the way, the traditional return value for both accept() and visit() is void.

Choosing Program Elements for Visitor Implementations

In general, the VP is really only useful when you are attempting to abstract not just over a set of objects, but over the set of actions that can be performed on those objects as well -- i.e., the case where each one of an arbitrary number of actions must be performed in a slightly different fashion, depending on the object at hand.

For example, suppose that we want to be able to get Information ("peek") at any of a number of objects, whose specific type is unknown (i.e., all we know is that each class implements AcceptorI. Implicit in this is the fact that the way we "peek" depends on what we're peeking at.

In addition to DoSomethingV, therefore, we also have a PeekV:


   class PeekV implements VisitorI {
      void visitA(A a) { /* Get information about a */ }
      void visitA(B b) { /* Get information about b */ }
      // ...
    }

If, however, the actions are specific to a particular class (that is, they're only defined for one object), then it doesn't make sense to abstract over them. Hence a VP would be an inappropriate designchoice. These sorts of actions are the ones that end up as the members functions of a class.


Visited times since February 23, 1999 (or the last crash).

kent@cs.uoregon.edu