s an architect working for my own company and the president of the Greater Lansing area .NET Users Group (glugnet.org) I’m constantly looking for customers and trying to increase technology awareness in the greater community. In essence, like every business person I am always looking for customers. A natural extension of my consulting business is matching employees to employers. As a result I began working on Motown-Jobs, a Web site where job seekers can post resumes and search for jobs posted by job owners (employers). When writing down the use cases for Motown-Jobs, I knew that I was very familiar with some aspects of matching employers to employees as I had been doing it for years. I was also sure that I might miss ways to capitalize on business opportunities because of expertise I didn?t have. To deal with my limited expertise and anticipate future revisions I hedged when I designed Motown-Jobs. Let me explain.
I want Motown-Jobs to permit employers to post jobs for a fee and employees to post resumes and search for jobs for free. I also suspect employers may want to know about the traffic their job listings generate, and future advertisers might be interested in general site traffic. In fact, site statistics of all kinds might be useful in unanticipated ways. When you can anticipate that a system may require changes but can’t define exactly what those changes will be, the challenge becomes one of programming defensively.
In this context, defensive programming means that you may want to add future behavior but without changing core classes constantly. For example, if a Job class is central to the system, then repeatedly updating the Job class would be disruptive. To respond to this potentiality defensively and proactively you can employ the Visitor behavior pattern.Think of the Visitor pattern as you would a census taker approaching a household. The census taker shows up, gathers some information, and goes away. (The going away part is good.) You could just as easily substitute the census taker with a garbage man, maid, repairman, or a favorite rich aunt who cleans all the time. The point is that the nature of a household?the number of walls, rooms, furniture, and regular occupants?doesn’t change that much; the visitor can drop in, do what that particular kind of visitor does?take census information, clean your house, or leave cash?and go away.
Interestingly enough, this metaphor holds up nicely because a visitor can be anything and do anything without changing the nature of the host. In this metaphor, the household plays host to any number and variety of visitors. Enough said.
Defining Hosts and Visitors
The Motown-Jobs code defines important classes for job listings, resumes, and advertisements. Each of these classes contains information about their contents. Job listing classes, for example, might contain information about the number of times they were viewed, when they were posted, and their individual expiration dates. You can define classes such as job listing and advertisement to have a relative dollar value. For job listing and advertisement you may want to determine the value of the time remaining for each listing. Finally, the future may hold new, useful, and valuable ways to use the data represented by instances of job listing, advertisement, and resume. From the perspective of the visitor pattern these classes represent hosts. In the future you may want to define new ways to drop in (visit) and collect information about instances of these classes.
The point is that by defining a facet of job listing, resume, and advertisement generically as hosts you can later define specific visitor classes to drop in and perform operations on instances of these classes.
Technically, implementing the Visitor pattern is quite straightforward. You define an abstract class or interface named Host?or IHost for interfaces?that has a single method named Accept that accepts a Visitor abstract class or an interface. Each Visitor contains methods called Visit for each kind of Host. You call the Host’s Accept method, passing in the Visitor, and the Host’s Accept method calls the Visitor.Visit method for that kind of Host. You code the Visit method for each Host to glean the information or perform the extended operation on the Host. The important point is that after implementing the Accept method you don’t need to change the hosts to add new behavior.
This kind of method dispatching is called double dispatching. You use double dispatching because hosts know only that visitors will be visiting and visitors know only that the host will ask the visitor to stay and visit. This interchange looks loosely like:
//Visitor: knock-knockHost.Accept(visitor)//Host: Hello, come on in and visit for a spellVisitor.Visit(this)
Subsequently, the visitor runs around poking at all your stuff and asking questions.
If all these folksy analogies aren’t helping, perhaps the UML class diagram for the Visitor pattern will clear it up for you. I encourage you to pick up a copy of the Gang of Four book, Design Patterns, published by Addison Wesley.
The Abstract Visitor Class
The examples in the upcoming listing separate out persistence and the user interface to permit us to focus on the implementation of the user pattern. The following class is an implementation of the abstract Host class:
' The abstract Host class.Namespace Visitor_VB.Patterns Public MustInherit Class Host Public MustOverride Sub Accept(ByVal visitor _ As Visitor) End ClassEnd Namespace
And here’s an implementation of an abstract Visitor class.
' The abstract Visitor class. Public MustInherit Class Visitor Public MustOverride Sub Visit(ByVal host As Job) Public MustOverride Sub Visit( _ ByVal host As [Resume]) Public MustOverride Sub Visit( _ ByVal host As Advertisement) Public MustOverride Sub Visit(ByVal host As Listing)End Class
|Author’s Note: In the preceding code, the Resume class is shown as [Resume] to distinguish the class name from the Resume reserved word in VB.NET.|
Next, simply define your host classes as subclasses of Host and define as many kinds of Visitor classes as you’d like and code the visitor classes to perform whatever operations you’d like.
The abstract Visitor class as coded above can visit Job, Resume, Listing, and Advertisement classes, so you need to define each of those classes to inherit from Host and override the Accept method. Keeping in mind that the code here doesn’t deal with persistence, Listing 1 shows a possible implementation of the Listing class. Note that this implementation used abstract classes for Host and Visitor rather than interfaces. .NET doesn’t support multiple inheritance, which results in a nested inheritance hierarchy with Host as the base class which is inherited by Listing, which is in turn inherited by each of Job, Resume, and Advertisement.
The Job, Resume, and Advertisement classes shown below inherit Host indirectly by inheriting from Listing, and each has its own implementation of the Accept method and the UpdateExpirationDate method defined in Listing.
Option Explicit On Option Strict On' If you place the classes below in separate files, ' repeat the preceding two lines in each file.Public Class Job Inherits Listing Public Sub New() MyBase.Content = "This is a job listing" End Sub Public Overrides Sub Accept( _ ByVal visitor As Visitor_VB.Patterns.Visitor) visitor.Visit(Me) End Sub Protected Overrides Sub UpdateExpirationDate() MyBase.ExpirationDate = _ MyBase.PostedDate.AddDays(30) End SubEnd ClassPublic Class [Resume] Inherits Listing Public Sub New() MyBase.Content = "This is a resume" End Sub Public Overrides Sub Accept( _ ByVal visitor As Visitor_VB.Patterns.Visitor) visitor.Visit(Me) End Sub Protected Overrides Sub UpdateExpirationDate() MyBase.ExpirationDate = _ MyBase.PostedDate.AddDays(365) End SubEnd ClassPublic Class Advertisement Inherits Listing Private FDaysPurchased As Integer Public Sub New() DaysPurchased = 60 MyBase.Content = "This is an advertisement" End Sub Public Overrides Sub Accept( _ ByVal visitor As Visitor_VB.Patterns.Visitor) visitor.Visit(Me) End Sub Protected Overrides Sub UpdateExpirationDate() MyBase.ExpirationDate = _ MyBase.PostedDate.AddDays(DaysPurchased) End Sub Public Property DaysPurchased() As Integer Get Return FDaysPurchased End Get Set(ByVal Value As Integer) FDaysPurchased = Value UpdateExpirationDate() End Set End PropertyEnd Class
Implementing the Visitor Pattern
So, now you have Job, Resume, and Advertisement classes that inherit from Listing (and indirectly Host) and each has an implementation of the Accept method. That’s the easiest part. The utility depends on how you implement the Visitor. Listing 2 shows an example of a Job, Resume, Advertisement, and Listing Visitor class.
Because Visitor is an abstract class, you have to implement each of the abstract Visit methods, as shown in Listing 2. The real work is done in the other members you add to the Visitor. This example gets the ListingCount, ViewingCount, TotalDaysRemaining, and the AverageDaysRemaining for each Listing, whether the listing is a Job, Resume, or Advertisement. Keep in mind that you could easily define future Visitors to do other things without ever changing the implementation of the hosts.
|Figure 1. Output from the Console Application: This figure shows statistics obtained by using the Visitor pattern to extract information from sample Listing objects.|
Listing 3 contains a sample console application that populates an array of Listings with some random Jobs, Resumes, and Advertisements. Next, it visits each Listing and gathers information using a Visitor and displays the results (see Figure 1).
You could probably achieve the same result by creating a typed collection of Listing objects, but you would have to change the typed collection each time you wanted to add new information-gathering features. Using the Visitor pattern eliminates this need. Even more beneficial would be to define Host and Visitor as interfaces, which would permit Visitor implementations to visit classes that do not have a common ancestor, and therefore would not fit neatly into a typed collection.
To recap, when you use the Visitor pattern, each class that plays host needs only a very simple Accept method. You can define as many visitors as you’d like at some arbitrary future time, and have them invoke any operation or gather any information you wish without changing the hosts. The result is that your hosts remain stable, while still supporting future additions. It is important to note that the hosts can be persisted classes and the GUI can be anything; I used simple hosts and a simple console application to avoid muddling up the pattern code.
Calculating Listing Days Remaining
To complete the example, a DaysRemaining class and a strongly typed collection of DaysRemaining makes it easier to calculate the days remaining in a listing by managing date spans as shown below.
Public Class DaysRemaining Private start, [end] As DateTime Public Sub New(ByVal start As DateTime, _ ByVal [end] As DateTime) Me.start = start Me.end = [end] End Sub Public ReadOnly Property Days() As Integer Get Return [end].Subtract(start).Days End Get End PropertyEnd ClassImports System.CollectionsPublic Class DaysRemainingCollection Inherits CollectionBase Public Sub New() End Sub Default Public Property Item( _ ByVal index As Integer) As DaysRemaining Get Return CType(List(index), DaysRemaining) End Get Set(ByVal Value As DaysRemaining) List(index) = Value End Set End Property Public Function Add(ByVal value As DaysRemaining) _ As Integer Return list.Add(value) End Function Public Function add(ByVal start As DateTime, _ ByVal [end] As DateTime) As Integer Return add(New DaysRemaining(start, [end])) End Function Public Function GetAverageDaysRemaining() _ As Integer If (List.Count <= 0) Then Return 0 Return Convert.ToInt32( _ GetSumDaysRemaining() / List.Count) End Function Public Function GetSumDaysRemaining() As Integer Dim total As Integer = 0 Dim o As DaysRemaining For Each o In Me total += o.Days Next Return total End FunctionEnd Class
The intent of the Visitor behavior pattern is to permit you to define new operations on objects without changing the implementation of those objects. Visitor uses a double dispatch mechanism so that hosts only need to define a simple Accept method that calls a Visit method on the Visitor. You can define as many visitors in the future as you need without ever changing the definition of the hosts.
You can define the Visitor pattern using either abstract classes or interfaces. If you use abstract classes then the hosts will have a common base class. If you use interfaces then hosts can have different supertypes (base classes).