Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
The goal of Visitor (the final, and arguably most complex,
pattern in GoF) is to separate the operations on a class hierarchy from the
hierarchy itself. This is quite an odd motivation because most of what we do in
object-oriented programming is to combine data and operations into objects, and
to use polymorphism to automatically select the correct variation of an
operation, depending on the exact type of an object.
With Visitor you extract the operations from inside your
class hierarchy into a separate, external hierarchy. The main hierarchy then
contains a visit( ) function that accepts any object from your
hierarchy of operations. As a result, you get two class hierarchies instead of
one. In addition, you ll see that your main hierarchy becomes very brittle if
you add a new class, you will force changes throughout the second hierarchy. GoF
says that the main hierarchy should thus rarely change. This constraint is
very limiting, and it further reduces the applicability of this pattern.
For the sake of argument, then, assume that you have a
primary class hierarchy that is fixed; perhaps it s from another vendor and you
can t make changes to that hierarchy. If you had the source code for the
library, you could add new virtual functions in the base class, but this is,
for some reason, not feasible. A more likely scenario is that adding new
virtual functions is somehow awkward, ugly or otherwise difficult to maintain. GoF
argues that distributing all these operations across the various node classes
leads to a system that s hard to understand, maintain, and change. (As you ll
see, Visitor can be much harder to understand, maintain and change.) Another
GoF argument is that you want to avoid polluting the interface of the main
hierarchy with too many operations (but if your interface is too fat, you
might ask whether the object is trying to do too many things).
The library creator must have foreseen, however, that you
will want to add new operations to that hierarchy, so that they can know to include
the visit( ) function.
So (assuming you really need to do this) the dilemma is that
you need to add member functions to the base class, but for some reason you
can t touch the base class. How do you get around this?
Visitor builds on the double-dispatching scheme shown in the
previous section. The Visitor pattern allows you to effectively extend the
interface of the primary type by creating a separate class hierarchy of type Visitor
to virtualize the operations performed on the primary type. The objects
of the primary type simply accept the visitor and then call the visitor s
dynamically bound member function. Thus, you create a visitor, pass it into the
primary hierarchy, and you get the effect of a virtual function. Here s a
simple example:
//: C10:BeeAndFlowers.cpp
// Demonstration of "visitor" pattern.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include <ctime>
#include <cstdlib>
#include "../purge.h"
using namespace std;
class Gladiolus;
class Renuculus;
class Chrysanthemum;
class Visitor {
public:
virtual void visit(Gladiolus* f) = 0;
virtual void visit(Renuculus* f) = 0;
virtual void visit(Chrysanthemum* f) = 0;
virtual ~Visitor() {}
};
class Flower {
public:
virtual void accept(Visitor&) = 0;
virtual ~Flower() {}
};
class Gladiolus : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
class Renuculus : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
class Chrysanthemum : public Flower {
public:
virtual void accept(Visitor& v) {
v.visit(this);
}
};
// Add the ability to produce a string:
class StringVal : public Visitor {
string s;
public:
operator const string&() { return s; }
virtual void visit(Gladiolus*) {
s = "Gladiolus";
}
virtual void visit(Renuculus*) {
s = "Renuculus";
}
virtual void visit(Chrysanthemum*) {
s = "Chrysanthemum";
}
};
// Add the ability to do "Bee" activities:
class Bee : public Visitor {
public:
virtual void visit(Gladiolus*) {
cout << "Bee and Gladiolus <<
endl;
}
virtual void visit(Renuculus*) {
cout << "Bee and Renuculus <<
endl;
}
virtual void visit(Chrysanthemum*) {
cout << "Bee and Chrysanthemum <<
endl;
}
};
struct FlowerGen {
Flower* operator()() {
switch(rand() % 3) {
default:
case 0: return new Gladiolus;
case 1: return new Renuculus;
case 2: return new Chrysanthemum;
}
}
};
int main() {
srand(time(0)); // Seed the random number generator
vector<Flower*> v(10);
generate(v.begin(), v.end(), FlowerGen());
vector<Flower*>::iterator it;
// It's almost as if I added a virtual function
// to produce a Flower string representation:
StringVal sval;
for(it = v.begin(); it != v.end(); it++) {
(*it)->accept(sval);
cout << string(sval) << endl;
}
// Perform "Bee" operation on all Flowers:
Bee bee;
for(it = v.begin(); it != v.end(); it++)
(*it)->accept(bee);
purge(v);
} ///:~
Flower is the primary hierarchy, and each subtype of Flower
can accept( ) a Visitor. The Flower hierarchy has no
operations other than accept( ), so all the functionality of the Flower
hierarchy is contained in the Visitor hierarchy. Note that the Visitor
classes must know about all the specific types of Flower, and if you add
a new type of Flower the entire Visitor hierarchy must be
reworked.
The accept( ) function in each Flower
begins a double dispatch as described in the previous section. The first
dispatch determines the exact type of Flower and the second determines
the exact type of Visitor. Once you know the exact types you can perform
an operation appropriate to both.
It s very unlikely that you ll use Visitor because its
motivation is unusual and its constraints are stultifying. The GoF examples are
not convincing the first is a compiler (not many people write compilers, and it
seems quite rare that Visitor is used within these compilers), and they
apologize for the other examples, saying you wouldn t actually use Visitor for
anything like this. You would need a stronger compulsion than that presented in
GoF to abandon an ordinary OO structure for Visitor what benefit does it really
buy you in exchange for much greater complexity and constraint? Why can t you
simply add more virtual functions in the base class when you discover you need
them? Or, if you really need to paste new functions into an existing hierarchy
and you are unable to modify that hierarchy, why not try multiple inheritance
first? (Even then, the likelihood of saving the existing hierarchy this way
is slim). Consider also that, to use Visitor, the existing hierarchy must
incorporate a visit( ) function from the beginning, because to add
it later would mean that you had permission to modify the hierarchy, so you
could just add ordinary virtual functions as you need them. No, Visitor must be
part of the architecture from the beginning, and to use it requires a
motivation greater than that in GoF.
We present Visitor here because we have seen it used when it
shouldn t be, just as multiple inheritance and any number of other approaches
have been used inappropriately. If you find yourself using Visitor, ask why.
Are you really unable to add new virtual functions in the base class? Do
you really want to be restricted from adding new types in your primary
hierarchy?
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |