Since RTTI is often misused by having it look for every single type in your system, it causes code to be non-extensible: when you add a new type, you have to go hunting for all the code
Trang 1public:
void visit(Aluminum* al) {
double v = al->weight() * al->value();
out << "value of Aluminum= " << v << endl;
"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
Trang 2void total(ostream& os) {
os << "Total weight Aluminum:"
// fillBin() still works, without changes, but
// different objects are prototyped:
Trang 3purge(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
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
RTTI considered harmful?
Various designs in this chapter attempt to remove RTTI, which might give you the impression
that it’s “considered harmful” (the condemnation used for poor goto) This isn’t true; it is the
misuse of RTTI that is the problem The reason our designs removed RTTI is because the
misapplication of that feature prevented extensibility, which contravened the stated goal of adding a new type to the system with as little impact on surrounding code as possible Since RTTI is often misused by having it look for every single type in your system, it causes code to
be non-extensible: when you add a new type, you have to go hunting for all the code in which RTTI is used, and if you miss any you won’t get help from the compiler
However, RTTI doesn’t automatically create non-extensible code Let’s revisit the trash
recycler once more This time, a new tool will be introduced, which I call a TypeMap It inherits from a map that holds a variant of type_info object as the key, and vector<Trash*>
as the value The interface is simple: you call addTrash( ) to add a new Trash pointer, and the map class provides the rest of the interface The keys represent the types contained in the associated vector The beauty of this design (suggested by Larry O’Brien) is that the
TypeMap dynamically adds a new key-value pair whenever it encounters a new type, so
Trang 4whenever you add a new type to the system (even if you add the new type at runtime), it adapts
The example will again build on the structure of the Trash types, and will use fillBin( ) to parse and insert the values into the TypeMap However, TypeMap is not a vector<Trash*>, and so it must be adapted to work with fillBin( ) by multiply inheriting from Fillable In addition, the Standard C++ type_info class is too restrictive to be used as a key, so a kind of wrapper class TypeInfo is created, which simply extracts and stores the type_info char*
representation of the type (making the assumption that, within the realm of a single compiler, this representation will be unique for each type)
//: C09:DynaTrash.cpp
//{L} TrashPrototypeInit
//{L} fillBin Trash TrashStatics
// Using a map of vectors and RTTI
// to automatically sort Trash into
// vectors This solution, despite the
// use of RTTI, is extensible
// Must adapt from type_info in Standard C++,
// since type_info is too restrictive:
template<class T> // T should be a base class
class TypeInfo {
string id;
public:
TypeInfo(T* t) : id(typeid(*t).name()) {}
const string& name() { return id; }
friend bool operator<(const TypeInfo& lv,
Trang 5TypeInfo is templatized because typeid( ) does not allow the use of void*, which would be
the most general way to solve the problem So you are required to work with some specific
class, but this class should be the most base of all the classes in your hierarchy TypeInfo must define an operator< because a map needs it to order its keys
Although powerful, the definition for TypeMap is simple; the addTrash( ) member function does most of the work When you add a new Trash pointer, the a TypeInfo<Trash> object for that type is generated This is used as a key to determine whether a vector holding objects
of that type is already present in the map If so, the Trash pointer is added to that vector If not, the TypeInfo object and a new vector are added as a key-value pair
An iterator to the map, when dereferenced, produces a pair object where the key (TypeInfo)
is the first member, and the value (Vector<Trash*>) is the second member And that’s all
An interesting thing about this design is that even though it wasn’t created to handle the
sorting, fillBin( ) is performing a sort every time it inserts a Trash pointer into bin When the Trash is thrown into bin it’s immediately sorted by TypeMap’s internal sorting mechanism Stepping through the TypeMap and operating on each individual vector becomes a simple
matter, and uses ordinary STL syntax
Trang 6As you can see, adding a new type to the system won’t affect this code at all, nor the code in
TypeMap This is certainly the smallest solution to the problem, and arguably the most elegant as well It does rely heavily on RTTI, but notice that each key-value pair in the map is
looking for only one type In addition, there’s no way you can “forget” to add the proper code
to this system when you add a new type, since there isn’t any code you need to add, other than that which supports the prototyping process (and you’ll find out right away if you forget that)
Summary
Coming up with a design such as TrashVisitor.cpp that contains a larger amount of code
than the earlier designs can seem at first to be counterproductive It pays to notice what you’re
trying to accomplish with various designs Design patterns in general strive to separate the things that change from the things that stay the same The “things that change” can refer to
many different kinds of changes Perhaps the change occurs because the program is placed into a new environment or because something in the current environment changes (this could be: “The user wants to add a new shape to the diagram currently on the screen”) Or, as in this case, the change could be the evolution of the code body While previous versions of the
trash-sorting example emphasized the addition of new types of Trash to the system,
TrashVisitor.cpp allows you to easily add new functionality without disturbing the Trash
hierarchy There’s more code in TrashVisitor.cpp, but adding new functionality to Visitor is
cheap If this is something that happens a lot, then it’s worth the extra effort and code to make
it happen more easily
The discovery of the vector of change is no trivial matter; it’s not something that an analyst can usually detect before the program sees its initial design The necessary information will probably not appear until later phases in the project: sometimes only at the design or
implementation phases do you discover a deeper or more subtle need in your system In the case of adding new types (which was the focus of most of the “recycle” examples) you might realize that you need a particular inheritance hierarchy only when you are in the maintenance phase and you begin extending the system!
One of the most important things that you’ll learn by studying design patterns seems to be an about-face from what has been promoted so far in this book That is: “OOP is all about
polymorphism.” This statement can produce the “two-year-old with a hammer” syndrome (everything looks like a nail) Put another way, it’s hard enough to “get” polymorphism, and once you do, you try to cast all your designs into that one particular mold
What design patterns say is that OOP isn’t just about polymorphism It’s about “separating the things that change from the things that stay the same.” Polymorphism is an especially
important way to do this, and it turns out to be helpful if the programming language directly supports polymorphism (so you don’t have to wire it in yourself, which would tend to make it
prohibitively expensive) But design patterns in general show other ways to accomplish the
basic goal, and once your eyes have been opened to this you will begin to search for more creative designs
Since the Design Patterns book came out and made such an impact, people have been
searching for other patterns You can expect to see more of these appear as time goes on Here
Trang 7are some sites recommended by Jim Coplien, of C++ fame (http://www.bell-labs.com/~cope),
who is one of the main proponents of the patterns movement:
Exercises
1 Using SingletonPattern.cpp as a starting point, create a class that manages
a fixed number of its own objects Assume the objects are database connections and you only have a license to use a fixed quantity of these at any one time
2 Create a minimal Observer-Observable design in two classes, without base
classes and without the extra arguments in Observer.h and the member functions in Observable.h Just create the bare minimum in the two classes, then demonstrate your design by creating one Observable and many Observers, and cause the Observable to update the Observers
3 Change InnerClassIdiom.cpp so that Outer uses multiple inheritance instead of the inner class idiom
4 Add a class Plastic to TrashVisitor.cpp
5 Add a class Plastic to DynaTrash.cpp
6 Explain how AbstractFactory.cpp demonstrates Double Dispatching and
the Factory Method
7 Modify ShapeFactory2.cpp so that it uses an Abstract Factory to create
different sets of shapes (for example, one particular type of factory object creates “thick shapes,” another creates “thin shapes,” but each factory object can create all the shapes: circles, squares, triangles etc.)
8 Create a business-modeling environment with three types of Inhabitant: Dwarf (for engineers), Elf (for marketers) and Troll (for managers) Now create a class called Project that creates the different inhabitants and causes them to interact( ) with each other using multiple dispatching
9 Modify the above example to make the interactions more detailed Each
Inhabitant can randomly produce a Weapon using getWeapon( ): a Dwarf uses Jargon or Play, an Elf uses InventFeature or
Trang 8SellImaginaryProduct, and a Troll uses Edict and Schedule You must
decide which weapons “win” and “lose” in each interaction (as in
PaperScissorsRock.cpp) Add a battle( ) member function to Project that takes two Inhabitants and matches them against each other Now create a meeting( ) member function for Project that creates groups of Dwarf, Elf and Manager and battles the groups against each other until only members
of one group are left standing These are the “winners.”
10 Implement Chain of Responsibility to create an “expert system” that solves
problems by successively trying one solution after another until one matches You should be able to dynamically add solutions to the expert system The test for solution should just be a string match, but when a solution fits, the expert system should return the appropriate type of problemSolver object What other pattern/patterns show up here?
Trang 911: Tools & topics
Tools created & used during the development of this book
and various other handy things
The code extractor
The code for this book is automatically extracted directly from the ASCII text version of this
book The book is normally maintained in a word processor capable of producing
camera-ready copy, automatically creating the table of contents and index, etc To generate the code
files, the book is saved into a plain ASCII text file, and the program in this section
automatically extracts all the code files, places them in appropriate subdirectories, and
generates all the makefiles The entire contents of the book can then be built, for each
compiler, by invoking a single make command This way, the code listings in the book can be
regularly tested and verified, and in addition various compilers can be tested for some degree
of compliance with Standard C++ (the degree to which all the examples in the book can
exercise a particular compiler, which is not too bad)
The code in this book is designed to be as generic as possible, but it is only tested under two
operating systems: 32-bit Windows and Linux (using the Gnu C++ compiler g++, which
means it should compile under other versions of Unix without too much trouble) You can
easily get the latest sources for the book onto your machine by going to the web site
www.BruceEckel.com and downloading the zipped archive containing all the code files and
makefiles If you unzip this you’ll have the book’s directory tree available However, it may
not be configured for your particular compiler or operating system In this case, you can
generate your own using the ASCII text file for the book (available at www.BruceEckel.com)
and the ExtractCode.cpp program in this section Using a text editor, you find the
CompileDB.txt file inside the ASCII text file for the book, edit it (leaving it the book’s text
file) to adapt it to your compiler and operating system, and then hand it to the ExtractCode
program to generate your own source tree and makefiles
You’ve seen that each file to be extracted contains a starting marker (which includes the file
name and path) and an ending marker Files can be of any type, and if the colon after the
comment is directly followed by a ‘!’ then the starting and ending marker lines are not
reproduced in the generated file In addition, you’ve seen the other markers {O}, {L}, and {T}
that have been placed inside comments; these are used to generate the makefile for each
subdirectory
Trang 10If there’s a mistake in the input file, then the program must report the error, which is the
error( ) function at the beginning of the program In addition, directory manipulation is not supported by the standard libraries, so this is hidden away in the class OSDirControl If you
discover that this class will not compile on your system, you must replace the non-portable
function calls in OSDirControl with equivalent calls from your library
Although this program is very useful for distributing the code in the book, you’ll see that it’s also a useful example in its own right, since it partitions everything into sensible objects and
also makes heavy use of the STL and the standard string class You may note that one or two
pieces of code might be duplicated from other parts of the book, and you might observe that some of the tools created within the program might have been broken out into their own
reusable header files and cpp files However, for easy unpacking of the book’s source code it
made more sense to keep everything lumped together in a single file
//: C10:ExtractCode.cpp
// Automatically extracts code files from
// ASCII text of this book
"where source is the ASCII file containing \n"
"the embedded tagged sourcefiles The ASCII \n"
"file must also contain an embedded compiler\n"
"configuration file called CompileDB.txt \n"
"See Thinking in C++, 2nd ed for details\n";
// Tool to remove the white space from both ends:
string trim(const string& s) {
if(s.length() == 0)
return s;
Trang 11// Manage all the error messaging:
void error(string problem, string message) {
static const string border(
cerr << in.rdbuf() << endl;
cerr << count << " Errors found" << endl;
cerr << "Messages in " << fname << endl;
}
};
// Created on first call to this function;
// Destructor reports total errors:
static ErrReport report;
report++;
report.errs << border << message << endl
<< "Problem spot: " << problem << endl;
}
///// OS-specific code, hidden inside a class:
#ifdef GNUC // For gcc under Linux/Unix
Trang 12static string getCurrentDir() {
Trang 13// - Manage code files -
// A CodeFile object knows everything about a
// particular code file, including contents, path
// information, how to compile, link, and test
// it, and which compilers it won't compile with
enum TType {header, object, executable, none};
class CodeFile {
TType _targetType;
string _rawName, // Original name from input
_path, // Where the source file lives
_file, // Name of the source file
_base, // Name without extension
_tname, // Target name
_testArgs; // Command-line arguments
vector<string>
lines, // Contains the file
_compile, // Compile dependencies
_link; // How to link the executable
set<string>
_noBuild; // Compilers it won't compile with
bool writeTags; // Whether to write the markers
// Initial makefile processing for the file:
void target(const string& s);
// For quoted #include headers:
void headerLine(const string& s);
// For special dependency tag marks:
void dependLine(const string& s);
public:
Trang 14CodeFile(istream& in, string& s);
const string& rawName() { return _rawName; }
const string& path() { return _path; }
const string& file() { return _file; }
const string& base() { return _base; }
const string& targetName() { return _tname; }
TType targetType() { return _targetType; }
const vector<string>& compile() {
const string& testArgs() { return _testArgs; }
// Add a compiler it won't compile with:
void addFailure(const string& failure) {
void CodeFile::target(const string& s) {
// Find the base name of the file (without
// the extension):
int lastDot = _file.find_last_of('.');
if(lastDot == string::npos) {
Trang 15error(s, "Missing extension");
exit(1);
}
_base = _file.substr(0, lastDot);
// Determine the type of file and target:
void CodeFile::headerLine(const string& s) {
int start = s.find('\"');
int end = s.find('\"', start + 1);
int len = end - start - 1;
_compile.push_back(s.substr(start + 1, len));
}
void CodeFile::dependLine(const string& s) {
const string linktag("//{L} ");
Trang 16string deps = trim(s.substr(linktag.length()));
while(true) {
int end = deps.find(' ');
string dep = deps.substr(0, end);
CodeFile::CodeFile(istream& in, string& s) {
// If false, don't write begin & end tags:
writeTags = (s[3] != '!');
// Assume a space after the starting tag:
_file = s.substr(s.find(' ') + 1);
// There will always be at least one colon:
int lastColon = _file.find_last_of(':');
if(lastColon == string::npos) {
error(s, "Missing path");
lastColon = 0; // Recover from error
cout << "path = [" << _path << "] "
<< "file = [" << _file << "]" << endl;
target(s); // Determine target type
// Look for specified link dependencies:
if(s2.find("//{L}") == 0) // 0: Start of line
dependLine(s2);
// Look for command-line arguments for test:
if(s2.find("//{T}") == 0) // 0: Start of line
_testArgs = s2.substr(strlen("//{T}") + 1);
// Look for quoted includes:
Trang 17error(s, "Error: new file started before"
" previous file concluded");
void CodeFile::dumpInfo(ostream& os) {
os << _path << ':' << _file << endl;
os << "target: " << _tname << endl;
// Information about each compiler:
vector<string> rules; // Makefile rules
set<string> fails; // Non-compiling files
string objExtension; // File name extensions
Trang 18string exeExtension;
// For OS-specific activities:
bool _dos, _unix;
// Store the information for all the compilers:
static map<string, CompilerData> compilerInfo;
static set<string> _compilerNames;
public:
CompilerData() : _dos(false), _unix(false) {}
// Read database of various compiler's
// information and failure listings for
// compiling the book files:
static void readDB(istream& in);
// For enumerating all the compiler names:
static set<string>& compilerNames() {
return _compilerNames;
}
// Tell this CodeFile which compilers
// don't work with it:
static void addFailures(CodeFile& cf);
// Produce the proper object file name
// extension for this compiler:
static string obj(string compiler);
// Produce the proper executable file name
// extension for this compiler:
static string exe(string compiler);
// For inserting a particular compiler's
// rules into a makefile:
static void
writeRules(string compiler, ostream& os);
// Change forward slashes to backward
// slashes if necessary:
static string
adjustPath(string compiler, string path);
// So you can ask if it's a Unix compiler:
static bool isUnix(string compiler) {
return compilerInfo[compiler]._unix;
}
// So you can ask if it's a dos compiler:
static bool isDos(string compiler) {
return compilerInfo[compiler]._dos;
}
// Display information (for debugging):
static void dump(ostream& os = cout);
Trang 19void CompilerData::readDB(istream& in) {
string compiler; // Name of current compiler
if(s.length() == 0) continue; // Blank line
if(s[0] == '#') continue; // Comment
if(s[0] == '{') { // Different compiler
compiler = s.substr(0, s.find('}'));
if(s[0] == '(') { // Object file extension
string obj = s.substr(1);
obj = trim(obj.substr(0, obj.find(')')));
compilerInfo[compiler].objExtension =obj;
continue;
}
if(s[0] == '[') { // Executable extension
string exe = s.substr(1);
exe = trim(exe.substr(0, exe.find(']')));
error("Compiler Information Database",
"unknown special directive: " + s);
Trang 20continue;
}
if(s[0] == '@') { // Makefile rule
string rule(s.substr(1)); // Remove the @
if(rule[0] == ' ') // Space means tab
Trang 21ext = '.' + ext; // Use '.' if it exists
string compiler, string path) {
// Use STL replace() algorithm:
Trang 22cout << "Won't compile with: " << endl;
copy(cd.fails.begin(), cd.fails.end(), out);
}
}
// - Manage makefile creation -
// Create the makefile for this directory, based
// on each of the CodeFile entries:
class Makefile {
vector<CodeFile> codeFiles;
// All the different paths
// (for creating the Master makefile):
static set<string> paths;
paths.insert(cf.path()); // Record all paths
// Tell it what compilers don't work with it:
CompilerData::addFailures(cf);
codeFiles.push_back(cf);
}
// Write the makefile for each compiler:
void writeMakefiles(string path);
// Create the master makefile:
static void writeMaster(string flag = "");
Trang 23void Makefile::createMakefile(
string compiler, string path) {
string // File name extensions:
"# For examples in directory "+ path + "\n"
"# using the " + compiler + " compiler\n"
"# Note: does not make files that will \n"
"# not compile with this compiler\n"
"# Invoke with: make -f "
+ compiler + ".makefile\n"
<< endl;
CompilerData::writeRules(compiler, makefile);
vector<string> makeAll, makeTest,
makeBugs, makeDeps, linkCmd;
// Write the "all" dependencies:
Trang 24// Create the link command:
int linkdeps = cf.link().size();
int compiledeps = cf.compile().size();
string objlist(cf.base() + obj + ": ");
for(int i = 0; i < compiledeps; i++)
// The "all" target:
copy(makeAll.begin(), makeAll.end(), mkos);
*mkos++ = "\n\n";
// Remove continuation marks from makeTest:
vector<string>::iterator si = makeTest.begin();
int bsl;
for(; si != makeTest.end(); si++)
if((bsl= (*si).find("\\\n")) != string::npos)
(*si).erase(bsl, strlen("\\"));
// Now print the "test" target:
Trang 25copy(makeTest.begin(), makeTest.end(), mkos);
*mkos++ = "\n\n";
// The "bugs" target:
copy(makeBugs.begin(), makeBugs.end(), mkos);
void Makefile::writeMaster(string flag) {
string filename = "makefile";
if(flag.length() != 0)
filename += '.' + flag;
ofstream makefile(filename.c_str());
makefile << "# Master makefile for "
"Thinking in C++, 2nd Ed by Bruce Eckel\n"
"# at http://www.BruceEckel.com\n"
"# Compiles all the code in the book\n"
"# Copyright notice in Copyright.txt\n\n"
"help: \n"
"\t@echo To compile all programs from \n"
"\t@echo Thinking in C++, 2nd Ed., type\n"
"\t@echo one of the following commands,\n"
"\t@echo according to your compiler:\n";
// Make for each compiler:
for(nit = n.begin(); nit != n.end(); nit++) {