Chapter 32. The Visitor Pattern

The Visitor pattern turns the tables on our object-oriented 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, because the operation wasn't considered when the hierarchy was designed or it would clutter the interface of the classes unnecessarily. The Visitor pattern is easier to explain using VB7, since polymorphism and inheritance make the code rather simpler. We'll discuss how to implement the Visitor in VB6 at the end of this chapter.

Motivation

While at first it may seem “unclean” to put operations inside one class that should be in another, there are good reasons for doing so. Suppose each of a number of drawing object classes has similar code for drawing itself. The drawing methods may be different, but they probably all use underlying utility functions that we 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 32-1.

A DrawObject and three of its subclasses

Figure 32-1. A DrawObject and three of its subclasses

Instead, we write a Visitor class that contains all the related draw methods and have it visit each of the objects in succession (Figure 32-2).

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

Figure 32-2. A Visitor class (Drawer) that visits each of three triangle classes

The first question that most people ask about this pattern is “What does visiting mean?” There is only one way that an outside class can gain access to another class, and that is by calling its public methods. In the Visitor case, visiting each class means that you are calling a method already installed for this purpose, called accept. The accept method has one argument: the instance of the visitor. In return, it calls the visit method of the Visitor, passing itself as an argument, as shown in Figure 32-3.

How the visit and accept methods interact

Figure 32-3. How the visit and accept methods interact

Putting it in simple code terms, every object that you want to visit must have the following method.

Public Sub accept(ByVal v As Visitor)
        v.visit(Me)
End Subb

In this way, the Visitor object receives a reference to each of the instances, one by one, and can then call its public methods to obtain data, perform calculations, generate reports, or just 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 have to 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 course or cannot change the source 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.

On the other hand, as we will see, Visitors are a good choice only when you do not expect many new classes to be added to your program.

Sample Code

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

Public Class Employee
    Dim sickDays As Integer, vacDays As Integer
    Dim salary As Single
    Dim name As String
    '-----
   Public Sub New(ByVal nm As String, ByVal sl As Single, _
                  ByVal vDays As Integer, ByVal sDays As Integer)
        name = nm
        salary = sl
        vacDays = vDays
        sickDays = sDays
    End Sub
    '-----
    Public Function getName() As String
        Return name
    End Function
    '-----
    Public Function getSalary() As Single
        Return salary
    End Function
    '-----
    Public Function getSickdays() As Integer
        Return sickDays
    End Function
    '-----
    Public Function getVacDays() As Integer
        Return vacDays
    End Function
    '-----
    Public Sub accept(ByVal v As Visitor)
        v.visit(Me)
    End Sub
End Class

Note that we have included the accept method in this class. Now let's suppose that we want to prepare a report on 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 VB is a strongly typed language, our base Visitor class needs to have a suitable abstract visit method for each kind of class in your program. In this first simple example, we only have Employees, so our basic abstract Visitor class is just the following.

Public MustInherit Class Visitor
    Public MustOverride Sub visit(ByVal emp As Employee)
End Class

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 Visitor we are going to write first just sums the vacation data for all our employees.

Public Class VacationVisitor
    Inherits Visitor
    Dim totalDays As Integer
    '-----
    Public Sub new()
        totalDays = 0
    End Sub
    '-----
    Public Function getTotalDays() As Integer
        getTotalDays = totalDays
    End Function
    '-----
    Public Overrides Sub visit(ByVal emp As Employee)
        totalDays = totalDays + emp.getVacDays
    End Sub
    Public Overrides Sub visit(ByVal bos As Boss)
        totalDays = totalDays + bos.getVacDays
    End Sub
End Class

Visiting the Classes

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

For i = 0 To empls.Length - 1
      empls(i).accept(vac)      'get the employee
Next i

List1.items.Add("Total vacation days=" & _
            vac.getTotalDays.toString)

Let's reiterate what happens for each visit.

  1. We move through a loop of all the 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 vacation days and adds them into the total.

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

Visiting Several Classes

The Visitor becomes more useful when there are a number of 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. Let's 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 day information.

Public Class Boss
    Inherits Employee
    Private bonusDays As Integer
    '-----
    Public Sub New(ByVal nm As String, _
                  ByVal sl As Single, _
         ByVal vDays As Integer, ByVal sDays As Integer)
        MyBase.New(nm, sl, vdays, sdays)
    End Sub
    '-----
    Public Sub setBonusDays(ByVal bdays As Integer)
        bonusdays = bdays
    End Sub
    '-----
    Public Function getBonusDays() As Integer
        Return bonusDays
    End Function
    '-----
    Public Overrides Sub accept(ByVal v As Visitor)
        v.visit(Me)
    End Sub
End Class

When we add a class to our program, we have to add it to our Visitor as well, so that the abstract template for the Visitor is now the following.

Public MustInherit Class Visitor
    Public MustOverride Sub visit(ByVal emp As Employee)
    Public MustOverride Overloads Sub visit(ByVal bos As Boss)
End Class

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

Public Class bVacationVisitor
    Inherits Visitor
    Private totalDays As Integer
    '-----
    Public Overrides Sub visit( _
                     ByVal emp As Employee)
        totalDays += emp.getVacDays
    End Sub
    '-----
    Public Overrides Sub visit(ByVal bos As Boss)
        totalDays += bos.getVacDays
        totalDays += bos.getBonusDays
    End Sub
    '-----
    Public Function getTotalDays() As Integer
        Return totalDays
    End Function
End Class

Note that 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 quite 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 your derived classes (Boss, Employee, etc.) must have its own accept method rather than calling one in its parent class. This is illustrated in the class diagram in Figure 32-4.

The two visitor classes visiting the Boss and Employee classes

Figure 32-4. The two visitor classes visiting the Boss and Employee classes

Bosses Are Employees, Too

We show in Figure 32-5 a simple application that carries out both Employee visits and Boss visits on the collection of Employees and Bosses. The original VacationVisitor will just treat Bosses as Employees and get only their ordinary vacation data. The bVacationVisitor will get both.

A simple application that performs the vacation visits described

Figure 32-5. A simple application that performs the vacation visits described

Dim i As Integer
Dim vac As New VacationVisitor()
Dim bvac As New bVacationVisitor()
For i = 0 To empls.Length - 1
    empls(i).accept(vac)      'get the employee
    empls(i).accept(bvac)
Next i
List1.items.Add("Total vacation days=" & _
    vac.getTotalDays.toString)
List1.items.Add("Total boss vacation days=" & _
    bvac.getTotalDays.tostring)

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

Catch-All Operations with Visitors

In the preceding cases, the Visitor class has a visit method for each visiting class, such as the following.

Public MustOverride Sub visit(ByVal emp As Employee)
Public MustOverride Sub visit(ByVal bos As Boss)

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 where that object type is recognized. This provides a way of specifying default visitor behavior.

Now every class must override accept(v) with its own implementation so 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 our company: the Manager. Managers are subclasses of Employees, and now they have the privileges formerly reserved for Bosses of extra vacation days. Bosses now have an additional reward—stock options. Now if we run the same program to compute vacation days but do not revise our Visitor to look for Managers, it will recognize them as mere Employees and count only their regular vacation and not their extra vacation days. However, the catch-all parent class is a good thing if subclasses may be added to the application from time to time and you want the visitor operations to continue to run without modification.

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

Public Overrides Sub visit(ByVal emp As Employee)
        totalDays += emp.getVacDays
        If TypeOf emp Is Manager Then
            Dim mgr As Manager = CType(emp, Manager)
            totaldays += mgr.getBonusDays
        End If
    End Sub

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 on the Visitor pattern is complete without mentioning that you are really dispatching a method twice 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 we write can carry out whatever operations we might think of using the data available in these classes.

Why Are We Doing This?

You may be asking yourself why we are jumping through these hoops when we could call the getVacationDays methods directly. By using this “callback” approach, we are implementing “double dispatching.” There is no requirement that the objects we visit be of the same or even of related types. Further, using this callback approach, you can have a different visit method called in the Visitor, depending on the actual type of class. This is harder to implement directly.

Further, if the list of objects to be visited in an ArrayList is a collection of different types, having different versions of the visit methods in the actual Visitor is the only way to handle the problem without specifically checking the type of each class.

Traversing a Series of Classes

The calling program that passes the class instances to the Visitor must know about all the existing instances of classes to be visited and must keep them in a simple structure such as an array or collection. 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 our simple example program, we used an array of objects, but any of the other methods would work equally well.

Writing a Visitor in VB6

In VB6, we will define the Visitor as an interface and define only the employee visit as being required.

'Interface Visitor
Public Sub visit(emp As Employee)
End Sub

The Employee class has an accept method much the same as in our VB7 version.

Public Sub accept(v As Visitor)
  v.visit Me
End Sub

To create a VacationVisitor, we create a class that implements the Visitor interface.

'Class VacationVisitor
Implements Visitor
Dim totalDays As Integer
'-----
Private Sub Class_Initialize()
 totalDays = 0
End Sub
'-----
Private Sub Visitor_visit(emp As Employee)
 totalDays = totalDays + emp.getVacDays
End Sub
'-----
Public Function getTotalDays() As Integer
 getTotalDays = totalDays
End Function

Then, to carry out the visiting and tabulate employee vacation days, we loop through and call each employee's accept method, much as before.

'loop through all the employees
For i = 1 To empls.Count
   Set empl = empls(i)
   empl.accept v         'get the employee
 Next i
List1.AddItem "Total vacation days=" & Str$(vac.getTotalDays)

In VB6, our Boss class implements the Employee interface rather than being derived from it and contains an instance of the Employee class.

'Class Boss
Implements Employee
Private empl As Employee
Private bonusDays As Integer
'-----
Private Sub Class_Initialize()
 Set empl = New Employee
End Sub
'-----
Private Sub Employee_accept(v As Visitor)
 empl.accept v
End Sub
'-----
Private Function Employee_getName() As String
 Employee_getName = empl.getName
End Function
'-----
Private Function Employee_getSalary() As Single
 Employee_getSalary = empl.getSalary
End Function
'-----
Private Function Employee_getSickdays() As Integer
 Employee_getSickdays = empl.getSickdays
End Function
'-----
Private Function Employee_getVacDays() As Integer
 Employee_getVacDays = empl.getVacDays
End Function
'-----
Private Sub Employee_init(nm As String, sl As Single, _
        vDays As Integer, sDays As Integer)
 empl.init nm, sl, vDays, sDays
End Sub
'-----
Public Sub setBonusdays(bday As Integer)
 bonusDays = bday
End Sub
'-----
Public Function getBonusDays() As Integer
 getBonusDays = bonusDays
End Function
'-----
Public Sub accept(v As Visitor)
 v.visit Me
End Sub

Note that this class has two accept methods. One is from implementing the Employee interface.

Private Sub Employee_accept(v As Visitor)
 empl.accept v
End Sub

Another is just for the Boss class.

Public Sub accept(v As Visitor)
 v.visit Me
End Sub

The problem that VB6 introduces is that you must refer to an object as being an Employee to use the Employee methods and refer to it as a Boss to use the Boss-specific methods. Thus, there cannot be a polymorphic set of visit methods in the visitor class for each class that is to visit. Instead, you must convert each object to the correct class to call that class's methods. Since we shouldn't have to know in advance which objects are Employees and which are Bosses, we just try to convert each Employee to a Boss and catch the error that is generated for classes where this is not legal.

Private Sub Compute_Click()
  Dim i As Integer
  Dim vac As New VacationVisitor
  Dim bvac As New bVacationVisitor
  Dim v As Visitor
  Dim bos As Boss
  Dim empl As Employee
  Set v = vac
  'loop through all the employees
  On Local Error GoTo noboss 'trap conversion errors
  For i = 1 To empls.Count
     Set empl = empls(i)
     empl.accept v         'get the employee
     empl.accept bvac      'and in box visitor
     Set bos = empls(i)
     bos.accept bvac       'get as boss
   nexti:
  Next i
  List1.AddItem "Total vacation days=" & Str$(vac.getTotalDays)
  List1.AddItem "Total boss vacation days=" & _
         Str$(bvac.getTotalDays)
Exit Sub
'error if non-boss converted
'just skips to bottom of loop
noboss:
  Resume nexti
End Sub

This approach is significantly less elegant, but it does allow you to use a Visitor-like approach in VB6.

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 suggests that the Visitor can provide additional functionality to a class without changing it. We prefer to say that a Visitor can add functionality to a collection of classes and encapsulate the methods 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 have provided. However, it 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, since each time you add new classes that must be visited, you have to add an abstract visit operation to the abstract Visitor class, and you must add an implementation for that class to each concrete Visitor you have written. Visitors can be powerful additions when the program reaches the point where many new classes are unlikely.

Visitors can be used very effectively in Composite systems, and the boss-employee system we just illustrated could well be a Composite like the one we used in the Composite chapter.

Thought Question

Thought Question

An investment firm's customer records consist of an object for each stock or other financial instrument 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

Visitor VB6 Visitor
VisitorVBNetVisitor VB7 Visitor
..................Content has been hidden....................

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