Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 52 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
52
Dung lượng
125,25 KB
Nội dung
417 z 516 gp As an example, let’s revisit the Shape system. 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 <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\n"; } void erase() { cout << "Circle::erase\n"; } ~Circle() { cout << "Circle::~Circle\n"; } }; class Square : public Shape { Square() {} friend class Shape; public: void draw() { cout << "Square::draw\n"; } void erase() { cout << "Square::erase\n"; } ~Square() { cout << "Square::~Square\n"; } }; 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* shlist[] = { "Circle", "Square", "Square", "Circle", "Circle", "Circle", "Square", "" }; int main() { vector<Shape*> shapes; try { for(char** cp = shlist; **cp; cp++) shapes.push_back(Shape::factory(*cp)); 418 z 516 } catch(Shape::BadShapeCreation e) { cout << e.what() << endl; purge(shapes); return 1; } 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; it happens to be a string in this case, 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 the previous example.) Comment 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.) Comment Polymorphic factories The static factory( ) member function in the previous example forces all the creation operations to be focused in one spot, so that’s the only place you need to change the code. This is certainly a reasonable solution, as it nicely encapsulates the process of creating objects. However, Design Patterns emphasizes that the reason for the Factory Method pattern is so that different types of factories can be derived from the basic factory. Factory Method is in fact a special type of polymorphic factory. However, Design Patterns does not provide an example, but instead just repeats the example used for the Abstract Factory. Here is ShapeFactory1.cpp modified so the Factory Methods are in a separate class as virtual functions: Comment //: C10:ShapeFactory2.cpp // Polymorphic Factory Methods #include <iostream> #include <map> #include <stdexcept> #include <string> #include <vector> #include " /purge.h" using namespace std; class Shape { public: virtual void draw() = 0; virtual void erase() = 0; virtual ~Shape() {} }; class ShapeFactory { virtual Shape* create() = 0; static map<string, ShapeFactory*> factories; public: virtual ~ShapeFactory() {} friend class ShapeFactoryInitializer; class BadShapeCreation : public logic_error { public: 419 z 516 BadShapeCreation(string type) : logic_error("Cannot create type " + type) {} }; static Shape* createShape(const string& id) throw(BadShapeCreation){ if(factories.find(id) != factories.end()) return factories[id]->create(); else throw BadShapeCreation(id); } }; // Define the static object: map<string, ShapeFactory*> ShapeFactory::factories; class Circle : public Shape { Circle() {} // Private constructor public: void draw() { cout << "Circle::draw\n"; } void erase() { cout << "Circle::erase\n"; } ~Circle() { cout << "Circle::~Circle\n"; } private: friend class ShapeFactoryInitializer; class Factory; friend class Factory; class Factory : public ShapeFactory { public: Shape* create() { return new Circle; } friend class ShapeFactoryInitializer; }; }; class Square : public Shape { Square() {} public: void draw() { cout << "Square::draw\n"; } void erase() { cout << "Square::erase\n"; } ~Square() { cout << "Square::~Square\n"; } private: friend class ShapeFactoryInitializer; class Factory; friend class Factory; class Factory : public ShapeFactory { public: Shape* create() { return new Square; } friend class ShapeFactoryInitializer; }; }; // Singleton to initialize the ShapeFactory: class ShapeFactoryInitializer { static ShapeFactoryInitializer si; ShapeFactoryInitializer() { ShapeFactory::factories["Circle"] = new Circle::Factory; ShapeFactory::factories["Square"] = new Square::Factory; } ~ShapeFactoryInitializer() { 420 z 516 delete ShapeFactory::factories["Circle"]; delete ShapeFactory::factories["Square"]; } }; // Static member definition: ShapeFactoryInitializer ShapeFactoryInitializer::si; char* shlist[] = { "Circle", "Square", "Square", "Circle", "Circle", "Circle", "Square", "" }; int main() { vector<Shape*> shapes; try { for(char** cp = shlist; **cp; cp++) shapes.push_back( ShapeFactory::createShape(*cp)); } catch(ShapeFactory::BadShapeCreation e) { cout << e.what() << endl; return 1; } for(size_t i = 0; i < shapes.size(); i++) { shapes[i]->draw(); shapes[i]->erase(); } purge(shapes); } ///:~ Now the Factory Method appears in its own class, ShapeFactory, as the virtual create( ). This is a private member function, which means it cannot be called directly but can be overridden. The subclasses of Shape must each create their own subclasses of ShapeFactory and override the create( ) member function to create an object of their own type. These factories are private, so that they are only accessible from the main Factory Method. This way, all client code must go through the Factory Method in order to create objects. Comment The actual creation of shapes is performed by calling ShapeFactory::createShape( ), which is a static member function that uses the map in ShapeFactory to find the appropriate factory object based on an identifier that you pass it. The factory is immediately used to create the shape object, but you could imagine a more complex problem in which the appropriate factory object is returned and then used by the caller to create an object in a more sophisticated way. However, it seems that much of the time you don’t need the intricacies of the polymorphic Factory Method, and a single static member function in the base class (as shown in ShapeFactory1.cpp) will work fine. Comment Notice that the ShapeFactory must be initialized by loading its map with factory objects, which takes place in the Singleton ShapeFactoryInitializer. So to add a new type to this design you must define the type, create a factory, and modify ShapeFactoryInitializer so that an instance of your factory is inserted in the map. This extra complexity again suggests the use of a static Factory Method if you don’t need to create individual factory objects. Comment Abstract factories The Abstract Factory pattern looks like the factory objects we’ve seen previously, with not one but several Factory Methods. Each of the Factory Methods creates a different kind of object. The idea is that when you create the factory object, you decide how all the objects created by that factory will be used. The example in Design Patterns implements portability across various graphical user interfaces (GUIs): you create a factory object appropriate to the GUI that you’re working with, and 421 z 516 from then on when you ask it for a menu, a button, a slider, and so on, it will automatically create the appropriate version of that item for the GUI. Thus, you’re able to isolate, in one place, the effect of changing from one GUI to another. Comment As another example, suppose you are creating a general-purpose gaming environment and you want to be able to support different types of games. Here’s how it might look using an Abstract Factory: Comment //: C10:AbstractFactory.cpp // A gaming environment #include <iostream> using namespace std; class Obstacle { public: virtual void action() = 0; }; class Player { public: virtual void interactWith(Obstacle*) = 0; }; class Kitty: public Player { virtual void interactWith(Obstacle* ob) { cout << "Kitty has encountered a "; ob->action(); } }; class KungFuGuy: public Player { virtual void interactWith(Obstacle* ob) { cout << "KungFuGuy now battles against a "; ob->action(); } }; class Puzzle: public Obstacle { public: void action() { cout << "Puzzle\n"; } }; class NastyWeapon: public Obstacle { public: void action() { cout << "NastyWeapon\n"; } }; // The abstract factory: class GameElementFactory { public: virtual Player* makePlayer() = 0; virtual Obstacle* makeObstacle() = 0; }; // Concrete factories: class KittiesAndPuzzles : public GameElementFactory { public: virtual Player* makePlayer() { return new Kitty; 422 z 516 } virtual Obstacle* makeObstacle() { return new Puzzle; } }; class KillAndDismember : public GameElementFactory { public: virtual Player* makePlayer() { return new KungFuGuy; } virtual Obstacle* makeObstacle() { return new NastyWeapon; } }; class GameEnvironment { GameElementFactory* gef; Player* p; Obstacle* ob; public: GameEnvironment(GameElementFactory* factory) : gef(factory), p(factory->makePlayer()), ob(factory->makeObstacle()) {} void play() { p->interactWith(ob); } ~GameEnvironment() { delete p; delete ob; delete gef; } }; int main() { GameEnvironment g1(new KittiesAndPuzzles), g2(new KillAndDismember); g1.play(); g2.play(); } /* Output: Kitty has encountered a Puzzle KungFuGuy now battles against a NastyWeapon */ ///:~ In this environment, Player objects interact with Obstacle objects, but the types of players and obstacles depend on the game. You determine the kind of game by choosing a particular GameElementFactory, and then the GameEnvironment controls the setup and play of the game. In this example, the setup and play are simple, but those activities (the initial conditions and the state change) can determine much of the game’s outcome. Here, GameEnvironment is not designed to be inherited, although it could possibly make sense to do that. Comment This example also illustrates double dispatching, which will be explained later. Virtual constructors One of the primary goals of using a factory is to organize your code so you don’t have to select an exact type of constructor when creating an object. That is, you can say, “I don’t know precisely w hat t yp e of ob j ect y ou are , but here’s the information. Create y ourself.” Comment 423 z 516 yp j y , y In addition, during a constructor call the virtual mechanism does not operate (early binding occurs). Sometimes this is awkward. For example, in the Shape program it seems logical that inside the constructor for a Shape object, you would want to set everything up and then draw( ) the Shape. The draw( ) function should be a virtual function, a message to the Shape that it should draw itself appropriately, depending on whether it is a circle, a square, a line, and so on. However, this doesn’t work inside the constructor, virtual functions resolve to the “local” function bodies when called in constructors. Comment If you want to be able to call a virtual function inside the constructor and have it do the right thing, you must use a technique to simulate a virtual constructor (which is a variation of the Factory Method). This is a conundrum. Remember, the idea of a virtual function is that you send a message to an object and let the object figure out the right thing to do. But a constructor builds an object. So a virtual constructor would be like saying, “I don’t know exactly what type of object you are, but build yourself anyway.” In an ordinary constructor, the compiler must know which VTABLE address to bind to the VPTR, and if it existed, a virtual constructor couldn’t do this because it doesn’t know all the type information at compile time. It makes sense that a constructor can’t be virtual because it is the one function that absolutely must know everything about the type of the object. Comment And yet there are times when you want something approximating the behavior of a virtual constructor. In the Shape example, it would be nice to hand the Shape constructor some specific information in the argument list and let the constructor create a specific type of Shape (a Circle, Square) with no further intervention. Ordinarily, you’d have to make an explicit call to the Circle, Square constructor yourself. Comment Coplien calls his solution to this problem “envelope and letter classes.” The “envelope” class is the base class, a shell that contains a pointer to an object of the base class. The constructor for the “envelope” determines (at runtime, when the constructor is called, not at compile time, when the type checking is normally done) what specific type to make, creates an object of that specific type (on the heap), and then assigns the object to its pointer. All the function calls are then handled by the base class through its pointer. So the base class is acting as a proxy for the derived class: Comment //: C10:VirtualConstructor.cpp #include <iostream> #include <string> #include <exception> #include <vector> using namespace std; class Shape { Shape* s; // Prevent copy-construction & operator= Shape(Shape&); Shape operator=(Shape&); protected: Shape() { s = 0; }; public: virtual void draw() { s->draw(); } virtual void erase() { s->erase(); } virtual void test() { s->test(); }; virtual ~Shape() { cout << "~Shape\n"; if(s) { [122] 424 z 516 cout << "Making virtual call: "; s->erase(); // Virtual call } cout << "delete s: "; delete s; // The polymorphic deletion } class BadShapeCreation : public exception { string reason; public: BadShapeCreation(string type) { reason = "Cannot create type " + type; } ~BadShapeCreation() throw() {} const char *what() const throw() { return reason.c_str(); } }; Shape(string type) throw(BadShapeCreation); }; class Circle : public Shape { Circle(Circle&); Circle operator=(Circle&); Circle() {} // Private constructor friend class Shape; public: void draw() { cout << "Circle::draw\n"; } void erase() { cout << "Circle::erase\n"; } void test() { draw(); } ~Circle() { cout << "Circle::~Circle\n"; } }; class Square : public Shape { Square(Square&); Square operator=(Square&); Square() {} friend class Shape; public: void draw() { cout << "Square::draw\n"; } void erase() { cout << "Square::erase\n"; } void test() { draw(); } ~Square() { cout << "Square::~Square\n"; } }; Shape::Shape(string type) throw(Shape::BadShapeCreation) { if(type == "Circle") s = new Circle; else if(type == "Square") s = new Square; else throw BadShapeCreation(type); draw(); // Virtual call in the constructor } char* shlist[] = { "Circle", "Square", "Square", "Circle", "Circle", "Circle", "Square", "" }; int main() { vector<Shape*> shapes; cout << "virtual constructor calls:" << endl; try { 425 z 516 for(char** cp = shlist; **cp; cp++) shapes.push_back(new Shape(*cp)); } catch(Shape::BadShapeCreation e) { cout << e.what() << endl; for(int j = 0; j < shapes.size(); j++) delete shapes[j]; return 1; } for(int i = 0; i < shapes.size(); i++) { shapes[i]->draw(); cout << "test\n"; shapes[i]->test(); cout << "end test\n"; shapes[i]->erase(); } Shape c("Circle"); // Create on the stack cout << "destructor calls:" << endl; for(int j = 0; j < shapes.size(); j++) { delete shapes[j]; cout << "\n \n"; } } ///:~ The base class Shape contains a pointer to an object of type Shape as its only data member. When you build a “virtual constructor” scheme, exercise special care to ensure this pointer is always initialized to a live object. Comment Each time you derive a new subtype from Shape, you must go back and add the creation for that type in one place, inside the “virtual constructor” in the Shape base class. This is not too onerous a task, but the disadvantage is you now have a dependency between the Shape class and all classes derived from it (a reasonable trade-off, it seems). Also, because it is a proxy, the base-class interface is truly the only thing the user sees. Comment In this example, the information you must hand the virtual constructor about what type to create is explicit: it’s a string that names the type. However, your scheme can use other information— for example, in a parser the output of the scanner can be handed to the virtual constructor, which then uses that information to determine which token to create. Comment The virtual constructor Shape(type) can only be declared inside the class; it cannot be defined until after all the derived classes have been declared. However, the default constructor can be defined inside class Shape, but it should be made protected so temporary Shape objects cannot be created. This default constructor is only called by the constructors of derived-class objects. You are forced to explicitly create a default constructor because the compiler will create one for you automatically only if there are no constructors defined. Because you must define Shape(type), you must also define Shape( ). Comment The default constructor in this scheme has at least one important chore—it must set the value of the s pointer to zero. This may sound strange at first, but remember that the default constructor will be called as part of the construction of the actual object—in Coplien’s terms, the “letter,” not the “envelope.” However, the “letter” is derived from the “envelope,” so it also inherits the data member s. In the “envelope,” s is important because it points to the actual object, but in the “letter,” s is simply excess baggage. Even excess baggage should be initialized, however, and if s is not set to zero by the default constructor called for the “letter,” bad things happen (as you’ll see later). Comment The virtual constructor takes as its argument information that completely determines the type of the object. Notice, though, that this type information isn’t read and acted upon until runtime, 426 z 516 whereas normally the compiler must know the exact type at compile time (one other reason this system effectively imitates virtual constructors). Comment The virtual constructor uses its argument to select the actual (“letter”) object to construct, which is then assigned to the pointer inside the “envelope.” At that point, the construction of the “letter” has been completed, so any virtual calls will be properly directed. Comment As an example, consider the call to draw( ) inside the virtual constructor. If you trace this call (either by hand or with a debugger), you can see that it starts in the draw( ) function in the base class, Shape. This function calls draw( ) for the “envelope” s pointer to its “letter.” All types derived from Shape share the same interface, so this virtual call is properly executed, even though it seems to be in the constructor. (Actually, the constructor for the “letter” has already completed.) As long as all virtual calls in the base class simply make calls to identical virtual functions through the pointer to the “letter,” the system operates properly. Comment To understand how it works, consider the code in main( ). To fill the vector shapes, “virtual constructor” calls are made to Shape. Ordinarily in a situation like this, you would call the constructor for the actual type, and the VPTR for that type would be installed in the object. Here, however, the VPTR used in each case is the one for Shape, not the one for the specific Circle, Square, or Triangle. Comment In the for loop where the draw( ) and erase( ) functions are called for each Shape, the virtual function call resolves, through the VPTR, to the corresponding type. However, this is Shape in each case. In fact, you might wonder why draw( ) and erase( ) were made virtual at all. The reason shows up in the next step: the base-class version of draw( ) makes a call, through the “letter” pointer s, to the virtual function draw( ) for the “letter.” This time the call resolves to the actual type of the object, not just the base class Shape. Thus, the runtime cost of using virtual constructors is one more virtual call every time you make a virtual function call. Comment To create any function that is overridden, such as draw( ), erase( ), or test( ), you must proxy all calls to the s pointer in the base class implementation, as shown earlier. This is because, when the call is made, the call to the envelope’s member function will resolve as being to Shape, and not to a derived type of Shape. Only when you make the proxy call to s will the virtual behavior take place. In main( ), you can see that everything works correctly, even when calls are made inside constructors and destructors. Comment Destructor operation The activities of destruction in this scheme are also tricky. To understand, let’s verbally walk through what happens when you call delete for a pointer to a Shape object—specifically, a Square—created on the heap. (This is more complicated than an object created on the stack.) This will be a delete through the polymorphic interface, as in the statement delete shapes[i] in main( ). Comment The type of the pointer shapes[i] is of the base class Shape, so the compiler makes the call through Shape. Normally, you might say that it’s a virtual call, so Square’s destructor will be called. But with the virtual constructor scheme, the compiler is creating actual Shape objects, even though the constructor initializes the letter pointer to a specific type of Shape. The virtual mechanism is used, but the VPTR inside the Shape object is Shape’s VPTR, not Square’s. This resolves to Shape’s destructor, which calls delete for the letter pointer s, which actually points to a Square object. This is again a virtual call, but this time it resolves to Square’s destructor. Comment With a destructor, however, C++ guarantees, via the compiler, that all destructors in the hierarchy are called. Square’s destructor is called first, followed by any intermediate destructors, in order, until finally the base-class destructor is called. This base-class destructor has code that says [...]... class object } } inner1; // Define a second inner class: class Inner2; friend class Outer::Inner2; class Inner2 : public Bingable { Outer* parent; public: Inner2(Outer* p) : parent(p) {} void bing() { cout . C1 0:VirtualConstructor.cpp #include <iostream> #include <string> #include <exception> #include <vector> using namespace std; class Shape { Shape* s; // Prevent copy-construction. protected so temporary Shape objects cannot be created. This default constructor is only called by the constructors of derived-class objects. You are forced to explicitly create a default constructor. which takes C+ +’s nested class one step further. Java has a built -in feature called an inner class, which is like a nested class in C+ +, but it has access to the nonstatic data of its containing