Export (0) Print
Expand All

© Microsoft Corporation. All rights reserved.

Visual Studio 6.0
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

March 2001

Programming with Class

Add Functionality to Objects

The Visitor design pattern allows you to extend objects while avoiding interface-compatibility issues.

by Richard Dalton

Reprinted with permission from Visual Basic Programmer's Journal, March 2001, Volume 11, Issue 3, Copyright 2001, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange.

One of the key principles of object-oriented (OO) design is combining data with its related operations. In fact, that combination is the essence of what constitutes an object. In general, the principle works well, but sticking too rigidly to the rules can sometimes lead to inflexible designs. You can often improve a design greatly by approaching it from a different perspective. In this column, I'll explain how to enhance OO designs using the Visitor design pattern, which decouples operations from data (download the code project).

Programmers commonly use a hierarchy of objects to represent a data structure such as a document. For example, a simple hierarchy might consist of an object called CDocument as the root of the hierarchy, with the CHeader, CBody, and CFooter objects becoming properties of CDocument (see Figure 1).

This is standard OO design, and it works well. You can add methods to the root object, and they apply to the entire hierarchy. If you want to save or print the document, you simply add appropriate methods to CDocument and make any required modifications to the other objects in the hierarchy. You build your object hierarchy, provide it with the ability to save and print itself, and ship it in a DLL so multiple applications can use it. In an ideal world, this object would work perfectly, meet all the needs of users, and never need to be changed.

However, being a programmer, you've suspected for some time that you don't live in an ideal world. You are not even a little surprised when, a few short months after you release your document object, you are informed you must extend it. The users love the app so much that they have a list of new features they'd like it to include. Some of your fellow programmers have started using your document object in their apps, and they also have a sizable wish list.

Your once-simple document object now must provide Pretty Print, Text Search, and Spell Checker features. As if this weren't enough, the users also want to be able to save the document in a choice of formats, including Extensible Markup Language (XML), because apparently their business will collapse if they don't embrace XML. You bite your tongue and get to work.

Adding such functionality to the objects in the hierarchy can mean tinkering with working code. More important, it can mean changing the interfaces of the objects, which is almost never a good idea. Ideally, you would like to expand the functionality of your objects without changing their interfaces. In other words, you need a way to decouple operations from your objects so you can add new operations without affecting the objects themselves.

 
Figure 1 | Model a Document Object Click here.

A Visitor to the Rescue
You can achieve the desired results by creating objects to represent the new operations. What you once thought of as mere operations are now objects in their own right that can have their own properties and methods. These objects are called Visitors.

As the name implies, a Visitor attaches itself to an object or a group of objects, performs its operation, then leaves. Once you make a data structure capable of accepting Visitors, you can add Visitors without any further modifications to the data structure. The actual work required to make a data structure Visitor-ready is limited. Only your imagination and that of your users limit the range of Visitors you can build for a given data structure.

For each data structure a Visitor will extend, you need to define a Visitor interface. You have no way of knowing what types of Visitors you'll develop for your data structure in the future, but you can ensure that all future Visitors will be able to work effectively with your data structure (see Figure 2).

The IHDocVisitor interface defines four methods that concrete visitor classes must implement. As you can see, there is one method for each type of object that the Visitor might be called upon to visit. The data structure dictates the Visitor interface clearly:

Public Sub VisitDoc(obj_Doc As CDocument)
End Sub

Public Sub VisitHeader(obj_Header As CHeader)
End Sub

Public Sub VisitBody(obj_Body As CBody)
End Sub

Public Sub VisitFooter(obj_Footer As CFooter)
End Sub

Public Property Get Result() As Variant
End Property

IHDocVisitor also provides a property called Result, which you can use to get the results out of the Visitor when it finishes traversing the data structure. This property is a variant because you aren't sure what those results will consist of.

The relationship between the data structure and the Visitor is important, because it shows that you can't define a generic Visitor interface. Each data structure requires its own Visitor interface. It is unlikely that you'd write a concrete Visitor to work with more than one data structure.

Figure 2 | Implement an Abstract Visitor Interface Click here.

 

Visual Basic supports only interface inheritance, so you don't actually inherit any functionality from the IHDocVisitor class. This lack of inheritance is perfectly acceptable in this case, because the idea of a default Visitor doesn't make much sense. It's difficult to imagine what kind of generic Visitor functionality your app could inherit.

The lack of functional inheritance is a stumbling block that requires special attention, because most published design patterns depend on it. Andrew J. Marshall gives an example of how you can use some ingenuity to overcome VB's apparent limitations with respect to design patterns.

A Visitor can't extend an object unless that object is capable of accepting the Visitor. For this reason, each object in your data structure must provide an Accept method. This Accept method is suitable for the Document object:

Public Sub Accept(Visitor As IHDocVisitor)
   ' Pass the document to the visitor
   Call Visitor.VisitDoc(Me)

   ' Allow each child object to accept the 
   ' visitor
   Call Header.Accept(Visitor)
   Call Body.Accept(Visitor)
   Call Footer.Accept(Visitor)
End Sub

The method receives an IHDocVisitor object. The Accept method's first task is to pass the document to the Visitor for processing. You've seen that the Visitor provides a method for each type of node in the data structure.

Introduce the Visitor
The Accept method's second responsibility is to introduce the Visitor to the document's child objects. The document's Accept method doesn't simply call the VisitHeader, VisitBody, or VisitFooter methods, because you must call these objects' Accept method so they can control access to their own children (if they have any). No matter how deeply nested your object hierarchy is, this simple technique ensures the Visitor gets to see all the hierarchy without having any knowledge of the structure.

When building an actual Visitor object, you don't have to concern yourself with how the data structure gets traversed. You simply code a routine for each type of object you visit and trust each will be called in the right order. CTextVisitor produces a simple formatted text version of the document (see Listing 1). Note that the Visitor maintains an internal string variable called ms_Text to keep track of your work as you traverse the data structure. Each of the methods adds to this variable on the basis of the object it's visiting. The Visitor could just as easily maintain a Word document, an Excel spreadsheet, or any object.

You now have a data structure, and you have a Visitor that traverses the structure to produce a report. All that remains is to bring the two together. This is a simple matter of creating an instance of the Visitor and passing it to your Document object's Accept method:

Dim obj_Visitor As IHDocVisitor
Set obj_Visitor = New CTextVisitor
Call mobj_Doc.Accept(obj_Visitor)

' Display the results
txt_Document.Text = obj_Visitor.Result

Set obj_Visitor = Nothing

The type of the obj_Visitor object is IHDocVisitor, so you can initialize it using any object that implements that interface. You can allow the user to choose which Visitor to use. For example, you can provide a Save As feature whereby the various formats are implemented as Visitors:

Dim obj_Visitor As IHDocVisitor

   If Me.opt_Text.Value = True Then
      Set obj_Visitor = New CTextVisitor
   Else
      Set obj_Visitor = New CXMLVisitor
   End If

   Call mobj_Doc.Accept(obj_Visitor)

   ' Display the results
   txt_Document.Text = _ 
      obj_Visitor.Result

Set obj_Visitor = Nothing

You have seen that Visitors are ideal when you want to produce a number of different reports based on the same data structure. You can add new reports by developing new Visitors. You don't need to make any changes to the underlying objects.

I am not suggesting that you should always separate data and operations. Methods are an essential and fundamental part of any OO design. The Visitor design pattern, like any other technique, is not suited to every problem, but is a valuable addition to your design tool chest.

Richard Dalton is a consultant Visual Basic programmer with an unhealthy interest in software architecture. He lives and works in Wicklow, Ireland, and you can reach him by e-mail at hello@rdalton.com.

Show:
© 2014 Microsoft