© Microsoft Corporation. All rights reserved.
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.
A Visitor to the Rescue
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:
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.
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:
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
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:
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:
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 email@example.com.