Improving
the design
The
solutions in
Design
Patterns
are organized around the question “What will change as this program
evolves?” This is usually the most important question that you can ask
about any design. If you can build your system around the answer, the results
will be two-pronged: not only will your system allow easy (and inexpensive)
maintenance, but you might also produce components that are reusable, so that
other systems can be built more cheaply. This is the promise of object-oriented
programming, but it doesn’t happen automatically; it requires thought and
insight on your part. In this section we’ll see how this process can
happen during the refinement of a system.
The
answer to the question “What will change?” for the recycling system
is a common one: more types will be added to the system. The goal of the
design, then, is to make this addition of types as painless as possible. In the
recycling program, we’d like to encapsulate all places where specific
type information is mentioned, so (if for no other reason) any changes can be
localized inside those encapsulations. It turns out that this process also
cleans up the rest of the code considerably.
“Make
more objects”
This
brings up a general object-oriented design principle that I first heard spoken
by Grady
Booch: “If the design is too complicated, make more objects.” This
is simultaneously counterintuitive and ludicrously simple, and yet it’s
the most useful guideline I’ve found. (You might observe that “make
more objects” is often equivalent to “add another level of
indirection.”) In general, if you find a place with messy code, consider
what sort of class would clean things up. Often the side effect of cleaning up
the code will be a system that has better structure and is more flexible.
Consider
first the place where
Trash
objects are created. In the above example, we’re conveniently using a
generator to create the objects. The generator nicely encapsulates the creation
of the objects, but the neatness is an illusion because in general we’ll
want to create the objects based on something more than a random number
generator. Some information will be available which will determine what kind of
Trash
object this should be. Because you generally need to make your objects by
examining some kind of information, if you’re not paying close attention
you may end up with
switch
statements (as in
TrashGen)
or cascaded
if
statements
scattered throughout your code. This is definitely messy, and also a place
where you must change code whenever a new type is added. If new types are
commonly added, a better solution is a single member function that takes all of
the necessary information and produces an object of the correct type, already
upcast to a
Trash
pointer. In
Design
Patterns
this is broadly referred to as a creational
pattern
(of which there are several). The specific pattern that will be applied here is
a variant of the Factory
Method
(“method” being a more OOPish way to refer to a member function).
Here, the factory method will be a
static
member of
Trash,
but more commonly it is a member function that is overridden in the derived
class.
The
idea of the factory method is that you pass it the essential information it
needs to know to create your object, then stand back and wait for the pointer
(already upcast to the base type) to pop out as the return value. From then on,
you treat the object polymorphically. Thus, you never even need to know the
exact type of object that’s created. In fact, the factory method hides it
from you to prevent accidental misuse. If you want to use the object without
polymorphism, you must explicitly use RTTI and casting.
But
there’s a little problem, especially when you use the more complicated
approach (not shown here) of making the factory method in the base class and
overriding it in the derived classes. What if the information required in the
derived class requires more or different arguments? “Creating more
objects” solves this problem. To implement the factory method, the
Trash
class gets a new member function called
factory( ).
To hide the creational data, there’s a new class called
Info
that contains all of the necessary information for the
factory( )
method to create the appropriate
Trash
object. Here’s a simple implementation of
Info:
class Info {
int type;
// Must change this to add another type:
static const int maxnum = 3;
double data;
public:
Info(int typeNum, double dat)
: type(typeNum % maxnum), data(dat) {}
};
An
Info
object’s only job is to hold information for the
factory( )
method. Now, if there’s a situation in which
factory( )
needs more or different information to create a new type of
Trash
object, the
factory( )
interface doesn’t need to be changed. The
Info
class can be changed by adding new data and new constructors, or in the more
typical object-oriented fashion of subclassing.
Here’s
the second version of the program with the factory method added. The
object-counting code has been removed; we’ll assume proper cleanup will
take place in all the rest of the examples.
//: C25:Recycle2.cpp
// Adding a factory method
#include <fstream>
#include <vector>
#include <typeinfo>
#include <cstdlib>
#include <ctime>
#include "sumValue.h"
#include "../purge.h"
using namespace std;
ofstream out("Recycle2.out");
class Trash {
double _weight;
void operator=(const Trash&);
Trash(const Trash&);
public:
Trash(double wt) : _weight(wt) { }
virtual double value() const = 0;
double weight() const { return _weight; }
virtual ~Trash() {}
// Nested class because it's tightly coupled
// to Trash:
class Info {
int type;
// Must change this to add another type:
static const int maxnum = 3;
double data;
friend class Trash;
public:
Info(int typeNum, double dat)
: type(typeNum % maxnum), data(dat) {}
};
static Trash* factory(const Info& info);
};
class Aluminum : public Trash {
static double val;
public:
Aluminum(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newval) {
val = newval;
}
~Aluminum() { out << "~Aluminum\n"; }
};
double Aluminum::val = 1.67F;
class Paper : public Trash {
static double val;
public:
Paper(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newval) {
val = newval;
}
~Paper() { out << "~Paper\n"; }
};
double Paper::val = 0.10F;
class Glass : public Trash {
static double val;
public:
Glass(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newval) {
val = newval;
}
~Glass() { out << "~Glass\n"; }
};
double Glass::val = 0.23F;
// Definition of the factory method. It must know
// all the types, so is defined after all the
// subtypes are defined:
Trash* Trash::factory(const Info& info) {
switch(info.type) {
default: // In case of overrun
case 0:
return new Aluminum(info.data);
case 1:
return new Paper(info.data);
case 2:
return new Glass(info.data);
}
}
// Generator for Info objects:
class InfoGen {
int typeQuantity;
int maxWeight;
public:
InfoGen(int typeQuant, int maxWt)
: typeQuantity(typeQuant), maxWeight(maxWt) {
srand(time(0));
}
Trash::Info operator()() {
return Trash::Info(rand() % typeQuantity,
static_cast<double>(rand() % maxWeight));
}
};
int main() {
vector<Trash*> bin;
// Fill up the Trash bin:
InfoGen infoGen(3, 100);
for(int i = 0; i < 30; i++)
bin.push_back(Trash::factory(infoGen()));
vector<Aluminum*> alBin;
vector<Paper*> paperBin;
vector<Glass*> glassBin;
vector<Trash*>::iterator sorter = bin.begin();
// Sort the Trash:
while(sorter != bin.end()) {
Aluminum* ap =
dynamic_cast<Aluminum*>(*sorter);
Paper* pp = dynamic_cast<Paper*>(*sorter);
Glass* gp = dynamic_cast<Glass*>(*sorter);
if(ap) alBin.push_back(ap);
if(pp) paperBin.push_back(pp);
if(gp) glassBin.push_back(gp);
sorter++;
}
sumValue(alBin);
sumValue(paperBin);
sumValue(glassBin);
sumValue(bin);
purge(bin); // Cleanup
} ///:~
In
the factory method
Trash::factory( ),
the determination of the exact type of object is simple, but you can imagine a
more complicated system in which
factory( )
uses an elaborate algorithm. The point is that it’s now hidden away in
one place, and you know to come to this place to make changes when you add new
types.
The
creation of new objects is now more general in
main( ),
and depends on “real” data (albeit created by another generator,
driven by random numbers). The generator object is created, telling it the
maximum type number and the largest “data” value to produce. Each
call to the generator creates an
Info
object which is passed into
Trash::factory( ),
which in turn produces some kind of
Trash
object and returns the pointer that’s added to the
vector<Trash*>
bin.
The
constructor for the
Info
object
is very specific and restrictive in this example. However, you could also
imagine a
vector
of arguments into the
Info
constructor
(or directly into a
factory( )
call, for that matter). This requires that the arguments be parsed and checked
at runtime, but it does provide the greatest flexibility.
You
can see from this code what “vector
of change” problem the factory is responsible for solving: if you add new
types to the system (the change), the only code that must be modified is within
the factory, so the factory isolates the effect of that change.
A
pattern for prototyping creation
A
problem with the above design is that it still requires a central location
where all the types of the objects must be known: inside the
factory( )
method. If new types are regularly being added to the system, the
factory( )
method must be changed for each new type. When you discover something like
this, it is useful to try to go one step further and move
all
of the activities involving that specific type – including its creation
– into the class representing that type. This way, the only thing you
need to do to add a new type to the system is to inherit a single class.
To
move the information concerning type creation into each specific type of
Trash,
the
“prototype”
pattern will be used. The general idea is that you have a master container of
objects, one of each type you’re interested in making. The
“prototype objects” in this container are used
only
for making new objects. In this case, we’ll name the object-creation
member function
clone( ).
When
you’re ready to make a new object, presumably you have some sort of
information that establishes the type of object you want to create. The
factory( )
method (it’s not required that you use factory with prototype, but they
comingle nicely) moves through the master container comparing your information
with whatever appropriate information is in the prototype objects in the master
container. When a match is found,
factory( )
returns
a clone of that object.
In
this scheme there is no hard-coded information for creation. Each object knows
how to expose appropriate information to allow matching, and how to clone
itself. Thus, the
factory( )
method doesn’t need to be changed when a new type is added to the system.
The
prototypes will be contained in a
static
vector<Trash*>
called
prototypes.
This is a
private
member of the base class
Trash.
The
friend
class
TrashPrototypeInit
is
responsible for putting the
Trash*
prototypes into the prototype list.
You’ll
also note that the
Info
class has changed. It now uses a
string
to act as type identification information. As you shall see, this will allow us
to read object information from a file when creating
Trash
objects.
//: C25:Trash.h
// Base class for Trash recycling examples
#ifndef TRASH_H
#define TRASH_H
#include <iostream>
#include <exception>
#include <vector>
#include <string>
class TypedBin; // For a later example
class Visitor; // For a later example
class Trash {
double _weight;
void operator=(const Trash&);
Trash(const Trash&);
public:
Trash(double wt) : _weight(wt) {}
virtual double value() const = 0;
double weight() const { return _weight; }
virtual ~Trash() {}
class Info {
std::string _id;
double _data;
public:
Info(std::string ident, double dat)
: _id(ident), _data(dat) {}
double data() const { return _data; }
std::string id() const { return _id; }
friend std::ostream& operator<<(
std::ostream& os, const Info& info) {
return os << info._id << ':' << info._data;
}
};
protected:
// Remainder of class provides support for
// prototyping:
static std::vector<Trash*> prototypes;
friend class TrashPrototypeInit;
Trash() : _weight(0) {}
public:
static Trash* factory(const Info& info);
virtual std::string id() = 0; // type ident
virtual Trash* clone(const Info&) = 0;
// Stubs, inserted for later use:
virtual bool
addToBin(std::vector<TypedBin*>&) {}
virtual void accept(Visitor&) {};
};
#endif // TRASH_H ///:~
The
basic part of the
Trash
class remains as before. The rest of the class supports the prototyping
pattern. The
id( )
member function returns a
string
that can be compared with the
id( )
of an
Info
object to determine whether this is the prototype that should be cloned (of
course, the evaluation can be much more sophisticated than that if you need
it). Both
id( )
and
clone( )
are pure
virtual
functions so they must be overridden in derived classes.
The
last two member functions,
addToBin( )
and
accept( ),
are “stubs” which will be used in later versions of the trash
sorting problem. It’s necessary to have these virtual functions in the
base class, but in the early examples there’s no need for them, so they
are not pure virtuals so as not to intrude.
The
factory( )
member
function has the same declaration, but the definition is what handles the
prototyping. Here is the implementation file:
//: C25:Trash.cpp {O}
#include "Trash.h"
using namespace std;
Trash* Trash::factory(const Info& info) {
vector<Trash*>::iterator it;
for(it = prototypes.begin();
it != prototypes.end(); it++) {
// Somehow determine the new type
// to create, and clone one:
if (info.id() == (*it)->id())
return (*it)->clone(info);
}
cerr << "Prototype not found for "
<< info << endl;
// "Default" to first one in the vector:
return (*prototypes.begin())->clone(info);
} ///:~
The
string
inside the
Info
object
contains the type name of the
Trash
to
be created; this
string
is compared to the
id( )
values of the objects in
prototypes.
If there’s a match, then that’s the object to create.
Of
course, the appropriate prototype object might not be in the
prototypes
list. In this case, the
return
in the inner loop is never executed and you’ll drop out at the end, where
a default value is created. It might be more appropriate to throw an exception
here.
As
you can see from the code, there’s nothing that knows about specific
types of
Trash.
The beauty of this design is that this code doesn’t need to be changed,
regardless of the different situations it will be used in.
Trash
subclasses
To
fit into the prototyping scheme, each new subclass of
Trash
must follow some rules. First, it must create a
protected
default constructor, so that no one but
TrashPrototypeInit
may
use it.
TrashPrototypeInit
is
a singleton, creating one and only one prototype
object
for each subtype. This guarantees that the
Trash
subtype will be properly represented in the
prototypes
container.
After
defining the “ordinary” member functions and data that the
Trash
object will actually use, the class must also override the
id( )
member (which in this case returns a
string
for comparison) and the
clone( )
function, which must know how to pull the appropriate information out of the
Info
object in order to create the object correctly.
Here
are the different types of
Trash,
each in their own file.
//: C25:Aluminum.h
// The Aluminum class with prototyping
#ifndef ALUMINUM_H
#define ALUMINUM_H
#include "Trash.h"
class Aluminum : public Trash {
static double val;
protected:
Aluminum() {}
friend class TrashPrototypeInit;
public:
Aluminum(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newVal) {
val = newVal;
}
std::string id() { return "Aluminum"; }
Trash* clone(const Info& info) {
return new Aluminum(info.data());
}
};
#endif // ALUMINUM_H ///:~
//: C25:Paper.h
// The Paper class with prototyping
#ifndef PAPER_H
#define PAPER_H
#include "Trash.h"
class Paper : public Trash {
static double val;
protected:
Paper() {}
friend class TrashPrototypeInit;
public:
Paper(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newVal) {
val = newVal;
}
std::string id() { return "Paper"; }
Trash* clone(const Info& info) {
return new Paper(info.data());
}
};
#endif // PAPER_H ///:~
//: C25:Glass.h
// The Glass class with prototyping
#ifndef GLASS_H
#define GLASS_H
#include "Trash.h"
class Glass : public Trash {
static double val;
protected:
Glass() {}
friend class TrashPrototypeInit;
public:
Glass(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newVal) {
val = newVal;
}
std::string id() { return "Glass"; }
Trash* clone(const Info& info) {
return new Glass(info.data());
}
};
#endif // GLASS_H ///:~
And
here’s a new type of
Trash:
//: C25:Cardboard.h
// The Cardboard class with prototyping
#ifndef CARDBOARD_H
#define CARDBOARD_H
#include "Trash.h"
class Cardboard : public Trash {
static double val;
protected:
Cardboard() {}
friend class TrashPrototypeInit;
public:
Cardboard(double wt) : Trash(wt) {}
double value() const { return val; }
static void value(double newVal) {
val = newVal;
}
std::string id() { return "Cardboard"; }
Trash* clone(const Info& info) {
return new Cardboard(info.data());
}
};
#endif // CARDBOARD_H ///:~
The
static
val
data members must be defined and initialized in a separate code file:
//: C25:TrashStatics.cpp {O}
// Contains the static definitions for
// the Trash type's "val" data members
#include "Trash.h"
#include "Aluminum.h"
#include "Paper.h"
#include "Glass.h"
#include "Cardboard.h"
double Aluminum::val = 1.67;
double Paper::val = 0.10;
double Glass::val = 0.23;
double Cardboard::val = 0.14;
///:~
There’s
one other issue: initialization of the static data members.
TrashPrototypeInit
must
create the prototype objects and add them to the
static
Trash::prototypes
vector.
So it’s
very
important that you control the order of initialization of the
static
objects, so the
prototypes
vector is created before any of the prototype objects, which depend on the
prior existence of
prototypes.
The most straightforward way to do this is to put all the definitions in a
single file, in the order in which you want them initialized.
TrashPrototypeInit
must be defined separately because it inserts the actual prototypes into the
vector,
and throughout the chapter we’ll be inheriting new types of
Trash
from the existing types. By making this one class in a separate file, a
different version can be created and linked in for the new situations, leaving
the rest of the code in the system alone.
//: C25:TrashPrototypeInit.cpp {O}
// Performs initialization of all the prototypes.
// Create a different version of this file to
// make different kinds of Trash.
#include "Trash.h"
#include "Aluminum.h"
#include "Paper.h"
#include "Glass.h"
#include "Cardboard.h"
// Allocate the static member object:
std::vector<Trash*> Trash::prototypes;
class TrashPrototypeInit {
Aluminum a;
Paper p;
Glass g;
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; ///:~
This
is taken a step further by making
TrashPrototypeInit
a singleton (the constructor is
private),
even though the class definition is not available in a header file so it would
seem safe enough to assume that no one could accidentally make a second instance.
Unfortunately,
this is one more separate piece of code you must maintain whenever you add a
new type to the system. However, it’s not too bad since the linker should
give you an error message if you forget (since
prototypes
is
defined in this file as well). The really difficult problems come when you
don’t
get any warnings or errors if you do something wrong.
Parsing
Trash from an external file
The
information about
Trash
objects will be read from an outside file. The file has all of the necessary
information about each piece of trash in a single entry in the form
Trash:weight.
There are multiple entries on a line, separated by commas:
//:! C25:Trash.dat
Glass:54, Paper:22, Paper:11, Glass:17,
Aluminum:89, Paper:88, Aluminum:76, Cardboard:96,
Aluminum:25, Aluminum:34, Glass:11, Glass:68,
Glass:43, Aluminum:27, Cardboard:44, Aluminum:18,
Paper:91, Glass:63, Glass:50, Glass:80,
Aluminum:81, Cardboard:12, Glass:12, Glass:54,
Aluminum:36, Aluminum:93, Glass:93, Paper:80,
Glass:36, Glass:12, Glass:60, Paper:66,
Aluminum:36, Cardboard:22,
///:~
To
parse this, the line is read and the string
member
function find( )
produces the index of the ‘
:’.
This is first used with the
string
member
function substr( )
to
extract the name of the trash type, and next to get the weight that is turned
into a
double
with
the
atof( )
function
(from
<cstdlib>). The
Trash
file
parser is placed in a separate file since it will be reused throughout this
chapter. To facilitate this reuse, the function
fillBin( )
which does the work takes as its first argument the name of the file to open
and read, and as its second argument a reference to an object of type
Fillable.
This uses what I’ve named the “interface” idiom at the
beginning of the chapter, and the only attribute for this particular interface
is that “it can be filled,” via a member function
addTrash( ).
Here’s the header file for
Fillable:
//: C25:Fillable.h
// Any object that can be filled with Trash
#ifndef FILLABLE_H
#define FILLABLE_H
class Fillable {
public:
virtual void addTrash(Trash* t) = 0;
};
#endif // FILLABLE_H ///:~
Notice
that it follows the interface idiom of having no non-static data members, and
all pure
virtual
member functions.
This
way, any class which implements this interface (typically using multiple
inheritance) can be filled using
fillBin( ).
Here’s the header file:
//: C25:fillBin.h
// Open a file and parse its contents into
// Trash objects, placing each into a vector
#ifndef FILLBIN_H
#define FILLBIN_H
#include <vector>
#include <string>
#include "Fillablevector.h"
void
fillBin(std::string filename, Fillable& bin);
// Special case to handle vector:
inline void fillBin(std::string filename,
std::vector<Trash*>& bin) {
Fillablevector fv(bin);
fillBin(filename, fv);
}
#endif // FILLBIN_H ///:~
The
overloaded version will be discussed shortly. First, here is the implementation:
//: C25:FillBin.cpp {O}
// Implementation of fillBin()
#include <fstream>
#include <string>
#include <cstdlib>
#include "fillBin.h"
#include "Fillable.h"
#include "../C17/trim.h"
#include "../require.h"
using namespace std;
void fillBin(string filename, Fillable& bin) {
ifstream in(filename.c_str());
assure(in, filename.c_str());
string s;
while(getline(in, s)) {
int comma = s.find(',');
// Parse each line into entries:
while(comma != string::npos) {
string e = trim(s.substr(0,comma));
// Parse each entry:
int colon = e.find(':');
string type = e.substr(0, colon);
double weight =
atof(e.substr(colon + 1).c_str());
bin.addTrash(
Trash::factory(
Trash::Info(type, weight)));
// Move to next part of line:
s = s.substr(comma + 1);
comma = s.find(',');
}
}
} ///:~
After
the file is opened, each line is read and parsed into entries by looking for
the separating comma, then each entry is parsed into its type and weight by
looking for the separating colon. Note the convenience of using the
trim( )
function from chapter 17 to remove the white space from both ends of a
string.
Once the type and weight are discovered, an
Info
object
is created from that data and passed to the
factory( ).
The result of this call is a
Trash*
which is passed to the
addTrash( )
function of the
bin
(which is the only function, remember, that a
Fillable
guarantees).
Anything
that supports the
Fillable
interface can be used with
fillBin.
Of course,
vector
doesn’t implement
Fillable,
so it won’t work. Since
vector
is used in most of the examples, it makes sense to add the second overloaded
fillBin( )
function that takes a
vector,
as seen previously in
fillBin.h.
But how to make a
vector<Trash*>
adapt
to the
Fillable
interface,
which says it must have an
addTrash( )
member
function? The key is in the word “adapt”; we use the adapter
pattern to create a class that has a
vector
and
is also
Fillable. By
saying “is also
Fillable,”
the hint is strong (is-a) to inherit from
Fillable.
But what about the
vector<Trash*>?
Should this new class inherit from that? We don’t actually want to be
making a new kind of
vector,
which would force everyone to only use our
vector
in this situation. Instead, we want someone to be able to have their own
vector
and say “please fill this.” So the new class should just keep a
reference to that
vector:
//: C25:Fillablevector.h
// Adapter that makes a vector<Trash*> Fillable
#ifndef FILLABLEVECTOR_H
#define FILLABLEVECTOR_H
#include <vector>
#include "Trash.h"
#include "Fillable.h"
class Fillablevector : public Fillable {
std::vector<Trash*>& v;
public:
Fillablevector(std::vector<Trash*>& vv)
: v(vv) {}
void addTrash(Trash* t) { v.push_back(t); }
};
#endif // FILLABLEVECTOR_H ///:~
You
can see that the only job of this class is to connect
Fillable’s
addTrash( )
member function to
vector’s
push_back( )
(that’s the “adapter” motivation). With this class in hand,
the overloaded
fillBin( )
member function can be used with a
vector
in
fillBin.h:
inline void fillBin(std::string filename,
std::vector<Trash*>& bin) {
Fillablevector fv(bin);
fillBin(filename, fv);
}
Notice
that the adapter object
fv
only exists for the duration of the function call, and it wraps
bin
in an interface that works with the other
fillBin( )
function.
This
approach works for any container class that’s used frequently.
Alternatively, the container can multiply inherit from
Fillable.
(You’ll see this later, in
DynaTrash.cpp.)
Recycling
with prototyping
Now
you can see the new version of the recycling solution using the prototyping
technique:
//: C25:Recycle3.cpp
//{L} TrashPrototypeInit
//{L} FillBin Trash TrashStatics
// Recycling with RTTI and Prototypes
#include <fstream>
#include <vector>
#include "Trash.h"
#include "Aluminum.h"
#include "Paper.h"
#include "Glass.h"
#include "fillBin.h"
#include "sumValue.h"
#include "../purge.h"
using namespace std;
ofstream out("Recycle3.out");
int main() {
vector<Trash*> bin;
// Fill up the Trash bin:
fillBin("Trash.dat", bin);
vector<Aluminum*> alBin;
vector<Paper*> paperBin;
vector<Glass*> glassBin;
vector<Trash*>::iterator it = bin.begin();
while(it != bin.end()) {
// Sort the Trash:
Aluminum* ap =
dynamic_cast<Aluminum*>(*it);
Paper* pp = dynamic_cast<Paper*>(*it);
Glass* gp = dynamic_cast<Glass*>(*it);
if(ap) alBin.push_back(ap);
if(pp) paperBin.push_back(pp);
if(gp) glassBin.push_back(gp);
it++;
}
sumValue(alBin);
sumValue(paperBin);
sumValue(glassBin);
sumValue(bin);
purge(bin);
} ///:~
The
process of opening the data file containing
Trash
descriptions and the parsing of that file have been wrapped into
fillBin( ),
so now it’s no longer a part of our design focus. You will see that
throughout the rest of the chapter, no matter what new classes are added,
fillBin( )
will continue to work without change, which indicates a good design.
In
terms of object creation, this design does indeed severely localize the changes
you need to make to add a new type to the system. However, there’s a
significant problem in the use of RTTI that shows up clearly here. The program
seems to run fine, and yet it never detects any cardboard, even though there is
cardboard in the list of trash data! This happens
because
of the use of RTTI, which looks for only the types that you tell it to look
for. The clue that RTTI
is being misused is that
every
type in the system
is
being tested, rather than a single type or subset of types. But if you forget
to test for your new type, the compiler has nothing to say about it.
As
you will see later, there are ways to use polymorphism instead when
you’re testing for every type. But if you use RTTI a lot in this fashion,
and you add a new type to your system, you can easily forget to make the
necessary changes in your program and produce a difficult-to-find bug. So
it’s worth trying to eliminate RTTI in this case, not just for aesthetic
reasons – it produces more maintainable code.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru