MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

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.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru