Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
When you discover that you need to add new types to a
system, the most sensible first step is to use polymorphism to create a common
interface to those new types. This separates the rest of the code in your
system from the knowledge of the specific types that you are adding. New types
can be added without disturbing existing code or so it seems. At first it
would appear that you need to change the code only in the place where you inherit
a new type, but this is not quite true. You must still create an object of your
new type, and at the point of creation you must specify the exact constructor
to use. Thus, if the code that creates objects is distributed throughout your
application, you have the same problem when adding new types you must still
chase down all the points of your code where type matters. It is the creation
of the type that matters here, rather than the use of the type (which is
taken care of by polymorphism), but the effect is the same: adding a new type
can cause problems.
The solution is to force the creation of objects to occur
through a common factory rather than to allow the creational code to be
spread throughout your system. If all the code in your program must go to this
factory whenever it needs to create one of your objects, all you must do when
you add a new object is modify the factory. This design is a variation of the
pattern commonly known as Factory Method. Since every object-oriented program
creates objects, and since it s likely you will extend your program by adding
new types, factories may be the most useful of all design patterns.
As an example, consider the commonly-used Shape
example. One approach to implementing a factory is to define a static
member function in the base class:
//: C10:ShapeFactory1.cpp
#include <iostream>
#include <stdexcept>
#include <cstddef>
#include <string>
#include <vector>
#include "../purge.h"
using namespace std;
class Shape {
public:
virtual void draw() = 0;
virtual void erase() = 0;
virtual ~Shape() {}
class BadShapeCreation : public logic_error {
public:
BadShapeCreation(string type)
: logic_error("Cannot create type " +
type) {}
};
static Shape* factory(const string& type)
throw(BadShapeCreation);
};
class Circle : public Shape {
Circle() {} // Private constructor
friend class Shape;
public:
void draw() { cout << "Circle::draw
<< endl; }
void erase() { cout << "Circle::erase
<< endl; }
~Circle() { cout << "Circle::~Circle
<< endl; }
};
class Square : public Shape {
Square() {}
friend class Shape;
public:
void draw() { cout << "Square::draw
<< endl; }
void erase() { cout << "Square::erase
<< endl; }
~Square() { cout << "Square::~Square
<< endl; }
};
Shape* Shape::factory(const string& type)
throw(Shape::BadShapeCreation) {
if(type == "Circle") return new Circle;
if(type == "Square") return new Square;
throw BadShapeCreation(type);
}
char* sl[] = { "Circle", "Square",
"Square",
"Circle", "Circle",
"Circle", "Square" };
int main() {
vector<Shape*> shapes;
try {
for(size_t i = 0; i < sizeof sl / sizeof sl[0];
i++)
shapes.push_back(Shape::factory(sl[i]));
} catch(Shape::BadShapeCreation e) {
cout << e.what() << endl;
purge(shapes);
return EXIT_FAILURE;
}
for(size_t i = 0; i < shapes.size(); i++) {
shapes[i]->draw();
shapes[i]->erase();
}
purge(shapes);
} ///:~
The factory( ) function takes an argument that
allows it to determine what type of Shape to create. Here, the argument is
a string, but it could be any set of data. The factory( ) is
now the only other code in the system that needs to be changed when a new type
of Shape is added. (The initialization data for the objects will
presumably come from somewhere outside the system and will not be a hard-coded
array as in this example.)
To ensure that the creation can only happen in the factory( ),
the constructors for the specific types of Shape are made private,
and Shape is declared a friend so that factory( ) has
access to the constructors. (You could also declare only Shape::factory( )
to be a friend, but it seems reasonably harmless to declare the entire
base class as a friend.) There is another important implication of this
design the base class, Shape, must now know the details about every
derived class a property that object-oriented designs try to avoid. For
frameworks or any class library that should support extension, this can quickly
become unwieldy, as the base class must be updated as soon as a new type is
added to the hierarchy. Polymorphic factories, described in the next
subsection, can be used to avoid this unfortunate circular dependency.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |