Visitor Design Pattern

COMP 310  Java Resources  Eclipse Resources

(Back to Design Patterns)

Summary: The Visitor Design Pattern enables one to decouple a data structure from the algorithms that process it.

The purpose of the Visitor Pattern is to encapsulate an operation that you want to perform on the elements of a data structure. In this way, you can change the operation being performed on a structure without the need of changing the classes of the elements that you are operating on. Using a Visitor pattern allows you to decouple the classes for the data structure and the algorithms used upon them.

Each node in the data structure "accepts" a visitor, which sends a message to the visitor which includes the node's class. The visitor will then execute its algorithm for that element. This process is known as "double dispatching." The node makes a call to the visitor, passing itself in, and the visitor executes its algorithm on the node. In double dispatching, the call made depends upon the type of the visitor and of the host (data structure node), not just of one component.

Figure 1: An example showing two concrete visitors that can be applied to either of two possible data structure hosts.
Example Visitor Implementation
Example Visitor Implementation
In the above UML diagram, we see a very simple abstract data structure, AElement, that comes in two possible concrete flavors, ConcreteElementA and ConcreteElementB. The data structure provides intrinsic, primitive behaviors that any processing algorithm might use to manipulate the data. Note that these operations, represented here simply as the operationA() and operationB() methods, are the atomic operations for manipulating the data upon which all more complex operations are built. The algorithms on the data are represented by the abstract AVisitor interface. Here, two possible algorithms are shown, ConcreteVisitor1 and ConcreteVisitor2.

In many traditional data processing algorithm architectures, "dumb" data is passed to a processing algorithm, which determines the specific nature of the data and then processes it accordingly. In the Visitor Design Pattern, the data is given the ability to determine how it is processed. Instead of requiring the algorithm to query the data object to dynamically determine its type, the vistior pattern recognizes that each data type, ConcreteElementA and ConcreteElementB here, already intrinsically knows its own type. The visitor simply provides methods for processing each respective data type and lets the data object determine which method to call. Since the data object intrinsically knows its own type, the determination of which method on the visitor algorithm to call is trivial. Thus the overall processing of the data involves a dispatch through the data object and then a subsequent dispatch into the appropriate processing method of the visitor. This is called "double dispatching". The code in the data object to accomplish this is in the exceedingly simple accept() method:

class ConcreteElementA extends AElement {
    public void accept(AVisitor v) {
        v.VisitConcreteElementA(this);
    }
    
    public void operationA() { 
        // an atomic, intrinsic opertion on ConcreteElementA objects.
    }
}

The key is the accept() method in the ConcreteElement classes. The body of this method shows the double dispatching call, where the visitor is passed in to the accept method, and that visitor is told to execute its visit method, and is handed the node by the node itself. This makes for very robust code, since all of the decision making as to what to execute where and when it taken care of by the dispatching. Nobody ever needs to check anything: they just do what it is that they do, with whatever they're handed. Pretty slick, eh?

One very nice thing about this double-dispatching technique is that it entirely replaces conditional statements, making for much more robust code. Because the data structure "host" passes itself in to the visitor, the visitor knows where to execute the algorithm, and because of the type of the host, it knows which method to run. This eliminates a lot of decision making: normally, you would have to decide (many times) at run-time what to do where, but here, all of the decision making has been completed in the design stage. No object ever has to ask any questions, they just do what it is that they do. The polymorhic dispatching takes care of all of the decision making.

One major application of this design pattern is LRStruct . Here, the IAlgo is the Visitor, and each element of a LRStruct is a node.

One key advantage of using the Visitor Pattern is that adding new operations to perform upon your data structure is very easy. All you have to do is create a new visitor and define the operation there. This is the result of the very distinct separation and decoupling of variant and invariant behavior in the Visitor pattern. The invariant behaviors are represented by the data structure elements and the abstract visitor. The variant behaviors are encapsulated in the concrete Visitors.

Every visitor has a method for every data structure element type. The data structure elements however, only deal with the abstract AVisitor, and hence only have one method (accept()) that deals with it. That method is overriden in each concrete element, which only calls its respective method in the visitor.

Fixed number of hosts limitation

The #1 complaint about the visitor pattern is that because the each visitor is required to have a method to service every possible concrete data host, the number and type of concrete hosts cannot be easily altered once the visitor pattern is implemented. The rebuttal to this complaint lies in the notion of the variant-invariant separation discussed above. Fundamentally, the visitor design patterns defines an invariant side, which includes all of the data structure's components. This design feature must be considered before the visitor design pattern is applied to a problem. To change the number or type of hosts is thus to modify a defined invariant in the system, which, in general, for any system, is asking for trouble. If the number and type of the hosts needs to variant, then perhaps the visitor design pattern is the wrong choice.

That said, there are two possible work-arounds:

  • Default behaviors in the abstract visitor: Instead of purely virtual methods in the abstrct visitor (AVisitor, here), concrete methods are implemented that provide default processing behavior for data host types. The concrete visitors need only override the methods they need to change. This work-around is only applicable when the system can be recompiled to add new data types.
  • Extended Visitor Design Pattern: Invented by Dr. Stephen Wong and Dr. Dung Nguyen, this extension of the visitor pattern utilizes a parameterized visit() method to dispatch to host-dependent lambda functions. This technique enables dynamically changing host types at run-time and has proven very useful in many enterprise-class solutions.

 

For more information:


Originally published in Connexions (CNX): https://web.archive.org/web/20130914054400/http://cnx.org/content/m26430/latest/

© 2023 by Stephen Wong