The Visitor pattern is used to specify the operations to perform during traversal [5]. A visitor allows behavior to be added to a composite structure without changing the existing class definitions. Visitors reduce the number of operations directly embedded within a class, thus preventing class definitions from becoming cluttered. Without a visitor, the implementation of a collaborative task may be spread over methods in different classes. Visitors group related operations performed on multiple classes (which need not be related through inheritance) together into one program component, supporting a task-based style of programming.
Figure 11: C++ visitor foundation code
For example, two operations to be performed on the equipment class structure include computing inventory of materials and calculating total cost of equipment [5]. Inventory simply accumulates a list of equipment objects encountered during traversal. Equipment cost is calculated by computing the net price of simple equipment and the discount price of composite equipment. Each task can be implemented with a visitor, which maps appropriate behavior to classes.
Figure 12: Visitor object model
Figure 11 demonstrates example C++ code for implementing a general visitor class, similar to that presented in [5]. The equipment classes must have an Accept method defined to allow a visitor to be activated for instances of the class. The EquipmentVisitor class must have a visit method defined for every concrete class for which a visit operation may occur. Each concrete subclass of equipment must define the Accept method. The Accept method for basic equipment subclasses such as Card and Drive simply activate the visitor, passing the current equipment object as an argument to the visit method (using the self reference). Composite subclasses such as Chassis and Cabinet would activate the visitor for themselves, and perform traversal to their parts to activate the visitor for each part. The Computer class would also require an Accept method, which would iterate to its equipment parts, invoking the Accept methods for them. Figure 12 depicts the relation between the EquipmentVisitor and Equipment classes, which basically takes the form of a call.
Figure 13: Subclassing the visitor class
Figure 13 defines two subclasses of EquipmentVisitor, namely PricingVisitor and InventoryVisitor. Each will map task-specific behavior to the equipment classes. Notice that although the same behavior is performed by the inventory visitor when either a Card or Chassis object is encountered, the limitations of the single dispatch mechanism still require a VisitXXX method to be defined for each concrete class XXX.
Vlissides points out several problems that arise from this approach to implementing visitors [25,26], noting that this implementation only works well when the class hierarchies are stable. For example, if a new subclass Keyboard is added to the Equipment hierarchy, a VisitKeyboard method must be added to each class in the EquipmentVisitor hierarchy that requires some task-specific action for Keyboard instances. The base class of the visitor hierarchy must have a VisitKeyboard method added to it as well.
Vlissides proposes an alternative technique to avoid some of the problems associated with the traditional visitor implementation. Rather than adding additional methods to the existing EquipmentVisitor class, he proposes the addition of a new visitor subclass to specifically handle the new Keyboard subclass. The drawback to the approach is that it requires the use of run-time type information. The new visitor subclass overrides the default visit operation that it inherits, to check if the current object being visited is a KeyBoard instance in which case it does some task-specific behavior rather than calling the default inherited visit method. As Vlissides notes, if many Equipment subclasses are added, the visitor subclass degrades into a case statement style of programming.
While Vlissides notes the inability of the traditional visitor approach to adapt to changes in the inheritance hierarchy, we note its inability to adapt to changes in the aggregation hierarchies since the Accept methods hard-code the whole-part relations between classes.
: Context relation between visitor and equipment
The essence of the visitor pattern is not particularly easy to abstract from the example C++ code shown in Figures 11 and 13. The true purpose of a visitor is to dynamically alter the implementation of one or more classes for the duration of some task. The visitor defines the task-specific meaning of the before and after methods that are executed during the traversal of a composite object. Thus, a visitor class is context-related to a group of classes that are covered by some traversal. Figure 14 depicts the relation between the visitors and the computer equipment class hierarchy.
Figure 15: Pricing and inventory visitors
Figure 15 contains the pricing and inventory visitors rewritten to utilize the context relation. The inventory visitor updates the before method for Equipment, which is inherited by its subclasses. The pricing visitor updates the before method for Equipment, as well as the before method for CompositeEquipment. Note that since the before and after methods are class-stored methods rather than instance-stored, there is only one implementation of each method, which is stored with the dynamic class definitions. Context attachment will update the dynamic class implementations, thus affecting all class instances.
Notice the before method that is updated in PricingVisitor refers to the total data member of the PricingVisitor class, as well as the netPrice method of the Equipment class. The method is similar to a multimethod, in that it can refer to the receiver object (instance of the Equipment class, referenced by this) as well as the context object (instance of the PricingVisitor class, referenced by context). Thus, the self reference is maintained to the original receiver of the message, yet the context object may partake in the behavior as well. This serves two purposes:
Figure 16: Program using visitors
The main program in Figure 16 instantiates the two visitors,
and invokes the traverse method for the Computer object. The
expression c->traverse{inv}();
indicates the traverse method
should be executed within the context of the inventory object inv.
Attaching the visitor to the call causes the dynamic Equipment class
definition to be updated based on the method update in the inventory visitor.
The visitor thus updates the before methods of the Equipment
class hierarchy for the duration of the traverse method invocation.
: Executing a traversal within the context of a visitor
Figure 17 depicts the relation between the method invocation and the inventory visitor. While the traverse method is executing, which simply traverses an Equipment object, each before and after message will be evaluated within the context surrounding the traverse invocation. Unlike the state and strategy patterns, where a context was attached to a specific object, the visitor pattern attaches a context to a traversal. This allows the context to affect messages sent to all objects for the duration of the traversal.
Notice that our approach to implementing the visitor pattern avoids the problems that Vlissides noted as arising from typical visitor implementations [25,26]. If a new subclass Keyboard is added to the Equipment hierarchy, the existing visitor code does not need to be modified to work with the new hierarchy. Also, if the aggregation or composition hierarchy changes, the existing traversal code does not need to be modified. We employ a simple navigation language to exploit the regularity often found in object traversal. For example, simply describing the target of navigation using the keyword to. By combining the context and traversal patterns, we can reuse a traversal with many different tasks, as well as reuse a task with many different traversals. Table 4 summarizes the different approaches to implementing the visitor pattern. Note that the language extensions required for the context and traversal patterns are easily implemented using a preprocessor.
Table 4: Comparison of visitor approaches
If a composite object is large, it might be preferable to compute several visitors during a single traversal. As an object may have its run-time behavior affected by many context objects, a method invocation may as well. Both the inventory and pricing visitors define implementations of the before method for the Equipment hierarchy. If we want to compute both visitors during a single traversal, the call should be executed within the context of each.
Method updates may be composed in an incremental or overriding manner. This does not refer to the inheritance relations of the Equipment hierarchy, rather it refers to the dynamic composition of a class method when updated by multiple contexts. The composition determines whether multiple implementations of the method will be executed per message. By default, contexts are attached in an overriding mode, in that a context serves to override or hide the current method implementation. However, incremental attachment will allow multiple method implementations to be executed for a single message, as is the case when executing several visitors per traversal. Of course, there are the standard compositional constraints involving return type and arguments. Incremental mode allows a behavior to be dynamically extended rather than overridden, which is in fact the goal of the Decorator pattern [5]. Figure 18 shows the main program rewritten with incremental context object attachment to compute both visitors during one traversal by using the + operator.
Figure 18: Incremental composition