Chapter 26. The Visitor Pattern

The Visitor pattern turns the tables on our OO model and creates an external class to act on data in other classes. This is useful when you have a polymorphic operation that cannot reside in the class hierarchy for some reason. For example, the operation wasn't considered when the hierarchy was designed or because it would clutter the interface of the classes unnecessarily.

Motivation

While at first it might seem "unclean" to put operations that should be inside of a class in another class instead, there are good reasons for doing this. Suppose that each of a number of drawing object classes has similar code for drawing itself. The drawing methods might differ, but they probably all use underlying utility functions that you might have to duplicate in each class. Further, a set of closely related functions is scattered throughout a number of different classes, as shown in Figure 26.1.

A DrawObject and three of its subclasses.

Figure 26.1. A DrawObject and three of its subclasses.

Instead, you can write a Visitor class that contains all of the related draw methods and have it visit each object in succession (Figure 26.2).

A Visitor class (Drawer) that visits each of three triangle classes.

Figure 26.2. A Visitor class (Drawer) that visits each of three triangle classes.

Most people who first read about this pattern typically ask, "What does visiting mean?" An outside class can gain access to another class in only one way: by calling its public methods. In the Visitor's case, visiting each case means that you call a method already installed for this purpose, called accept. The accept method has one argument: the instance of the visitor. It then calls the visit method of the Visitor, passing itself as an argument, as shown in Figure 26.3.

How the visit and accept methods interact.

Figure 26.3. How the visit and accept methods interact.

Put in simple code terms, every object that you want to visit must have the following method:

public void accept(Visitor v) {
        v.visit(this);
    }

In this way, the Visitor object receives a reference to each instance, one by one, and can then call its public methods to obtain data, perform calculations, generate reports, or only draw the object on the screen. Of course, if the class does not have an accept method, you can subclass it and add one.

When to Use the Visitor Pattern

You should consider using a Visitor pattern when you want to perform an operation on the data contained in a number of objects that have different interfaces. Visitors are also valuable if you must perform a number of unrelated operations on these classes. Visitors are a useful way to add function to class libraries or frameworks for which you either do not have the source or cannot change thesource for other technical (or political) reasons. In these latter cases, you simply subclass the classes of the framework and add the accept method to each subclass.

Visitors are a good choice, however, only when you do not expect many new classes to be added to your program. This is shown shortly.

Sample Code

Let's consider a simple subset of the Employee problem discussed in the Composite pattern. We have a simple Employee object that maintains a record of the employee's name, salary, number of vacation days taken, and number of sick days taken. A simple version of this class is the following:

public class Employee {
    private int       sickDays, vacDays;
    private float     Salary;
    private String    Name;

    public Employee(String name, float salary, int vacdays,
                    int sickdays) {
        vacDays = vacdays;
        sickDays = sickdays;
        Salary = salary
        Name = name;
    }
    public String getName() {
        return Name;
    }
    public int getSickdays() {
        return sickDays;
    }
    public int getVacDays() {
        return vacDays;
    }
    public float getSalary() {
        return Salary;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

Note that we have included the accept method in this class. Now let's suppose that we want to prepare a report of the number of vacation days that all employees have taken so far this year. We could just write some code in the client to sum the results of calls to each Employee's getVacDays function, or we could put this function into a Visitor.

Since Java is a strongly typed language, our base Visitor class needs to have a suitable abstract visit method for each kind of class in the program. In the following first example of our basic abstract visitor class, we have only Employees.

public abstract class Visitor {
    public abstract void visit(Employee emp);
    public abstract void visit(Boss emp);
}

Notice that there is no indication what the Visitor does with each class in either the client classes or the abstract Visitor class. We can in fact write a whole lot of Visitors that do different things to the classes in our program. The first Visitor that we write sums the vacation data for all employees.

public class VacationVisitor extends Visitor {
    protected int total_days;
    public VacationVisitor() {
        total_days = 0;
    }
    //--------------
    public void visit(Employee emp) {
        total_days += emp.getVacDays();
    }
    //--------------
    public void visit(Boss boss) {
        total_days += boss.getVacDays();
    }
    //--------------
    public int getTotalDays() {
        return total_days;
    }
}

Visiting the Classes

Now all that we must do to compute the total vacation days taken is to go through a list of the employees, visit each of them, and then ask the Visitor for the total.

        VacationVisitor vac = new VacationVisitor();
        for (int i = 0; i < employees.length; i++) {
            employees[i].accept(vac);
    }
    total.setText(new Integer(vac.getTotalDays()).toString());

Here's what happens for each visit:

  1. We move through a loop of all Employees.

  2. The Visitor calls each Employee's accept method.

  3. That instance of Employee calls the Visitor's visit method.

  4. The Visitor fetches the value for the number of vacation days and adds that number to the total.

  5. The main program prints out the total number of days when the loop is complete.

Visiting Several Classes

The Visitor becomes more useful when we have several different classes with different interfaces and we want to encapsulate how we get data from these classes. Let's extend our vacation days model by introducing a new Employee type called Boss. We further suppose that at this company, Bosses are rewarded with bonus vacation days (instead of money). So the Boss class has a couple of extra methods to set and obtain the bonus vacation days information.

public class Boss extends Employee {
    private int bonusDays;
    public Boss(String name, float salary, int vacdays,
            int sickdays) {
        super(name, salary, vacdays, sickdays);
    }
    public void setBonusDays (int bonus) {
        bonusDays = bonus;
    }
    public int getBonusDays() {
        return bonusDays;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

When we add a class to this program, we must add it to our Visitor as well. The abstract template for the Visitor is now as follows:

public abstract class Visitor {
    public abstract void visit(Employee emp);
    public abstract void visit(Boss emp);
}

This says that any concrete Visitor classes that we write must provide polymorphic visit methods for both the Employee and the Boss classes. In the case of the vacation day counter, we need to ask the Boss for both regular and bonus days taken, so the visits are now different. We'll write a new bVacationVisitor class that takes this difference into account.

public class bVacationVisitor extends Visitor {
    int total_days;

    public bVacationVisitor() {
        total_days = 0;
    }
    //--------------
    public int getTotalDays() {
        return total_days;
    }
//--------------
    public void visit(Boss boss) {
        total_days += boss.getVacDays();
        total_days += boss.getBonusDays();
    }
    //--------------
    public void visit(Employee emp) {
        total_days += emp.getVacDays();
    }
}

While in this case Boss is derived from Employee, it need not be related at all as long as it has an accept method for the Visitor class. It is important, however, that you implement a visit method in the Visitor for every class you will be visiting and not count on inheriting this behavior, since the visit method from the parent class is an Employee rather than a Boss visit method. Likewise, each of our derived classes (Boss, Employee, and so on) must have its own accept method rather than calling one in its parent class. This is illustrated in the class diagram in Figure 26.4.

The two Visitor classes visiting the Boss and Employee classes.

Figure 26.4. The two Visitor classes visiting the Boss and Employee classes.

Bosses Are Employees, Too

Figure 26.5 shows an application that carries out both Employee visits and Boss visits on the collection of Employees and Bosses. The original VacationVisitor will treat Bosses as Employees and get only their ordinary vacation data. The bVacationVisitor will get both ordinary and bonus vacation days.

An application that performs the vacation visits.

Figure 26.5. An application that performs the vacation visits.

    VacationVisitor vac = new VacationVisitor();
    bVacationVisitor bvac = new bVacationVisitor();
        for (int i = 0; i < employees.length; i++)    {
            employees[i].accept(vac);
            employees[i].accept(bvac);
    }
total.setText(new Integer(vac.getTotalDays()).toString());
btotal.setText(new Integer(bvac.getTotalDays()).toString());

The two lines of displayed data represent the two sums that are computed when the user clicks on the Vacation button.

Catch-All Operations Using Visitors

In all of the previous cases, the Visitor class had a visit method for each visiting class, such as the following:

    public void visit(Employee emp) {}
    public void visit(Boss emp) {}

However, if you start subclassing your Visitor classes and adding new classes that might visit, you should recognize that some visit methods might not be satisfied by the methods in the derived class. These might instead "fall through" to methods in one of the parent classes in which that object type is recognized. This provides a way of specifying default Visitor behavior.

Note that every class must override accept(v) with its own implementation so that the return call v.visit(this) returns an object this of the correct type and not of the superclass's type.

Let's suppose that we introduce another layer of management into the company, discussed earlier in the chapter: the Manager. Managers are subclasses of Employees, and now they have the privileges formerly reserved for Bosses, that is, extra vacation days. And Bosses now have an additional reward: stock options. If we run the same program to compute vacation days but do not revise out Visitor to look for Managers, it will recognize Managers as mere Employees and count only their regular vacation days and not their extra vacation days. However, the catch-all parent class is good to have if subclasses might be added to the application from time to time and we want the Visitor operations to continue to run without modification.

There are three ways to integrate the new Manager class into the visitor system. In one, we could define a ManagerVisitor. In the second, we could use the BossVisitor to handle both. However, there could be conditions when it is not desirable to modify the Visitor structure continually. In that third case, you could simply test for this in the EmployeeVisitor class.

public void visit (Employee emp) {
    total_days += emp.getVacDays();
    //special test in catch-all parent class
    if(emp instanceof Manager)
        total_days += ((Manager) emp).getBonusDays ();
}

While this seems "unclean" at first compared to defining classes properly, it can provide a method of catching special cases in derived classes without writing whole new Visitor program hierarchies. This catch-all approach is discussed in some detail in the book Pattern Hatching [Vlissides, 1998].

Double Dispatching

No discussion of the Visitor pattern is complete without mentioning that you are really dispatching a method twice in order for the Visitor to work. The Visitor calls the polymorphic accept method of a given object, and the accept method calls the polymorphic visit method of the Visitor. It is this bidirectional calling that allows you to add more operations on any class that has an accept method, since each new visitor class can carry out whatever operations you might think of using the data available in these classes.

Traversing a Series of Classes

In order to visit a series of classes, the calling program that passes the class instances to the Visitor must know about all of the existing instances of classes to be visited and must keep them in a simple structure such as an array or Vector. Another possibility would be to create an Enumeration of these classes and pass it to the Visitor. Finally, the Visitor itself could keep the list of objects that it is to visit. In the earlier example program, we used an array of objects, but any of the other methods would work equally well.

Consequences of the Visitor Pattern

The Visitor pattern is useful when you want to encapsulate fetching data from a number of instances of several classes. Design Patterns suggest that the Visitor can provide additional functionality to a class without changing it. However, it's preferable to say that a Visitor can add functionality to a collection of classes and encapsulate the methods that it uses.

The Visitor is not magic, however, and cannot obtain private data from classes; it is limited to the data available from public methods. This might force you to provide public methods that you would otherwise not provide. However, the Visitor can obtain data from a disparate collection of unrelated classes and utilize it to present the results of a global calculation to the user program.

It is easy to add new operations to a program using Visitors, since the Visitor contains the code instead of each of the individual classes. Further, Visitors can gather related operations into a single class rather than forcing you to change or derive classes to add these operations. This can make the program simpler to write and maintain.

Visitors are less helpful during a program's growth stage. This is because each time that you add new classes that must be visited, you must add an abstract visit operation to the abstract Visitor class and an implementation for that class to each concrete Visitor that you've written. Visitors can be powerful additions when the program reaches the point at which many new classes are unlikely.

They also can be used very effectively in Composite systems. For example, the boss-employee system just illustrated could well be a Composite like the one used in Chapter 11, The Composite Pattern.

Thought Question

  1. An investment firm's customer records consist of an object for each stock or other financial instrument that each investor owns. The object contains a history of the purchase, sale, and dividend activities for that stock. Design a Visitor pattern to report on net end-of-year profit or loss on stocks sold during the year.

Programs on the CD-ROM

Program Description

VisitorVacationDisplay.java

Displays the result of visiting a series of Employee and Boss objects and shows their vacation days.

VisitorCatchAllVacationDisplay.java

Displays vacation days for Employee, Manager, and Boss classes, using Employee as catch-all.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset