Applying
the visitor pattern
Now
consider applying a design pattern with an entirely different goal to the
trash-sorting problem. As demonstrated earlier in this chapter, the visitor
pattern’s goal is to allow the addition of new polymorphic operations to
a frozen inheritance hierarchy.
For
this pattern, we are no longer concerned with optimizing the addition of new
types of
Trash
to
the system. Indeed, this pattern makes adding a new type of
Trash
more
complicated. It looks like this:
Now,
if
t
is a
Trash
pointer
to an
Aluminum
object, the code:
PriceVisitor pv;
t->accept(pv);
causes
two polymorphic member function calls: the first one to select
Aluminum’s
version of
accept( ),
and the second one within
accept( )
when the specific version of
visit( )
is called dynamically using the base-class
Visitor
pointer
v. This
configuration means that new functionality can be added to the system in the
form of new subclasses of
Visitor.
The
Trash
hierarchy
doesn’t need to be touched. This is the prime benefit of the visitor
pattern: you can add new polymorphic functionality to a class hierarchy without
touching that hierarchy (once the
accept( )
methods have been installed). Note that the benefit is helpful here but not
exactly what we started out to accomplish, so at first blush you might decide
that this isn’t the desired solution.
But
look at one thing that’s been accomplished: the visitor solution avoids
sorting from the master
Trash
sequence into individual typed sequences. Thus, you can leave everything in the
single master sequence and simply pass through that sequence using the
appropriate visitor to accomplish the goal. Although this behavior seems to be
a side effect of visitor, it does give us what we want (avoiding RTTI).
The
double
dispatching in the visitor pattern takes care of determining both the type of
Trash
and
the type of
Visitor.
In
the following example, there are two implementations of
Visitor:
PriceVisitor
to both determine and sum the price, and
WeightVisitor
to keep track of the weights.
You
can see all of this implemented in the new, improved version of the recycling
program. As with
DoubleDispatch.cpp,
the
Trash
class has had an extra member function stub (
accept( ))
inserted
in it to allow for this example.
Since
there’s nothing concrete in the
Visitor
base class, it can be created as an
interface:
//: C25:Visitor.h
// The base interface for visitors
// and template for visitable Trash types
#ifndef VISITOR_H
#define VISITOR_H
#include "Trash.h"
#include "Aluminum.h"
#include "Paper.h"
#include "Glass.h"
#include "Cardboard.h"
class Visitor {
public:
virtual void visit(Aluminum* a) = 0;
virtual void visit(Paper* p) = 0;
virtual void visit(Glass* g) = 0;
virtual void visit(Cardboard* c) = 0;
};
// Template to generate visitable
// trash types by inheriting from originals:
template<class TrashType>
class Visitable : public TrashType {
protected:
Visitable () : TrashType(0) {}
friend class TrashPrototypeInit;
public:
Visitable(double wt) : TrashType(wt) {}
// Remember "this" is pointer to current type:
void accept(Visitor& v) { v.visit(this); }
// Override clone() to create this new type:
Trash* clone(const Trash::Info& info) {
return new Visitable(info.data());
}
};
#endif // VISITOR_H ///:~
As
before, a different version of the initialization file is necessary:
//: C25:VisitorTrashPrototypeInit.cpp {O}
#include "Visitor.h"
std::vector<Trash*> Trash::prototypes;
class TrashPrototypeInit {
Visitable<Aluminum> a;
Visitable<Paper> p;
Visitable<Glass> g;
Visitable<Cardboard> c;
TrashPrototypeInit() {
Trash::prototypes.push_back(&a);
Trash::prototypes.push_back(&p);
Trash::prototypes.push_back(&g);
Trash::prototypes.push_back(&c);
}
static TrashPrototypeInit singleton;
};
TrashPrototypeInit
TrashPrototypeInit::singleton; ///:~
The
rest of the program creates specific
Visitor
types and sends them through a single list of
Trash
objects:
//: C25:TrashVisitor.cpp
//{L} VisitorTrashPrototypeInit
//{L} FillBin Trash TrashStatics
// The "visitor" pattern
#include <iostream>
#include <fstream>
#include "Visitor.h"
#include "fillBin.h"
#include "../purge.h"
using namespace std;
ofstream out("TrashVisitor.out");
// Specific group of algorithms packaged
// in each implementation of Visitor:
class PriceVisitor : public Visitor {
double alSum; // Aluminum
double pSum; // Paper
double gSum; // Glass
double cSum; // Cardboard
public:
void visit(Aluminum* al) {
double v = al->weight() * al->value();
out << "value of Aluminum= " << v << endl;
alSum += v;
}
void visit(Paper* p) {
double v = p->weight() * p->value();
out <<
"value of Paper= " << v << endl;
pSum += v;
}
void visit(Glass* g) {
double v = g->weight() * g->value();
out <<
"value of Glass= " << v << endl;
gSum += v;
}
void visit(Cardboard* c) {
double v = c->weight() * c->value();
out <<
"value of Cardboard = " << v << endl;
cSum += v;
}
void total(ostream& os) {
os <<
"Total Aluminum: $" << alSum << "\n" <<
"Total Paper: $" << pSum << "\n" <<
"Total Glass: $" << gSum << "\n" <<
"Total Cardboard: $" << cSum << endl;
}
};
class WeightVisitor : public Visitor {
double alSum; // Aluminum
double pSum; // Paper
double gSum; // Glass
double cSum; // Cardboard
public:
void visit(Aluminum* al) {
alSum += al->weight();
out << "weight of Aluminum = "
<< al->weight() << endl;
}
void visit(Paper* p) {
pSum += p->weight();
out << "weight of Paper = "
<< p->weight() << endl;
}
void visit(Glass* g) {
gSum += g->weight();
out << "weight of Glass = "
<< g->weight() << endl;
}
void visit(Cardboard* c) {
cSum += c->weight();
out << "weight of Cardboard = "
<< c->weight() << endl;
}
void total(ostream& os) {
os << "Total weight Aluminum:"
<< alSum << endl;
os << "Total weight Paper:"
<< pSum << endl;
os << "Total weight Glass:"
<< gSum << endl;
os << "Total weight Cardboard:"
<< cSum << endl;
}
};
int main() {
vector<Trash*> bin;
// fillBin() still works, without changes, but
// different objects are prototyped:
fillBin("Trash.dat", bin);
// You could even iterate through
// a list of visitors!
PriceVisitor pv;
WeightVisitor wv;
vector<Trash*>::iterator it = bin.begin();
while(it != bin.end()) {
(*it)->accept(pv);
(*it)->accept(wv);
it++;
}
pv.total(out);
wv.total(out);
purge(bin);
} ///:~
Note
that the shape of
main( )
has changed again. Now there’s only a single
Trash
bin. The two
Visitor
objects are accepted into every element in the sequence, and they perform their
operations. The visitors keep their own internal data to tally the total
weights and prices.
Finally,
there’s no run-time type identification other than the inevitable cast to
Trash
when pulling things out of the sequence.
One
way you can distinguish this solution from the double dispatching solution
described previously is to note that, in the double dispatching solution, only
one of the overloaded methods,
add( ),
was overridden when each subclass was created, while here
each
one of the overloaded
visit( )
methods is overridden in every subclass of
Visitor.
More
coupling?
There’s
a lot more code here, and there’s definite coupling between the
Trash
hierarchy and the
Visitor
hierarchy. However, there’s also high cohesion within the respective sets
of classes: they each do only one thing (
Trash
describes
trash, while
Visitor
describes
actions performed on
Trash),
which is an indicator of a good design. Of course, in this case it works well
only if you’re adding new
Visitors,
but it gets in the way when you add new types of
Trash. Low
coupling between classes and high cohesion within a class is definitely an
important design goal. Applied mindlessly, though, it can prevent you from
achieving a more elegant design. It seems that some classes inevitably have a
certain intimacy with each other. These often occur in pairs that could perhaps
be called couplets,
for example, containers and iterators. The
Trash-Visitor
pair above appears to be another such couplet.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru