1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in Cplus plus (P28) pptx

50 48 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 163,9 KB

Nội dung

Chapter 16: Design Patterns 451 virtual void update(Observable* o, Argument * arg) = 0; }; #endif // OBSERVER_H ///:~ Since Observer interacts with Observable in this approach, Observable must be declared first. In addition, the Argument class is empty and only acts as a base class for any type of argument you wish to pass during an update. If you want, you can simply pass the extra argument as a void* ; you’ll have to downcast in either case but some folks find void* objectionable. Observer is an “interface” class that only has one member function, update( ) . This function is called by the object that’s being observed, when that object decides its time to update all it’s observers. The arguments are optional; you could have an update( ) with no arguments and that would still fit the observer pattern; however this is more general – it allows the observed object to pass the object that caused the update (since an Observer may be registered with more than one observed object) and any extra information if that’s helpful, rather than forcing the Observer object to hunt around to see who is updating and to fetch any other information it needs. The “observed object” that decides when and how to do the updating will be called the Observable : //: C09:Observable.h // The Observable class #ifndef OBSERVABLE_H #define OBSERVABLE_H #include "Observer.h" #include <set> class Observable { bool changed; std::set<Observer*> observers; protected: virtual void setChanged() { changed = true; } virtual void clearChanged(){ changed = false; } public: virtual void addObserver(Observer& o) { observers.insert(&o); } virtual void deleteObserver(Observer& o) { observers.erase(&o); } virtual void deleteObservers() { observers.clear(); } virtual int countObservers() { Chapter 16: Design Patterns 452 return observers.size(); } virtual bool hasChanged() { return changed; } // If this object has changed, notify all // of its observers: virtual void notifyObservers(Argument* arg=0) { if(!hasChanged()) return; clearChanged(); // Not "changed" anymore std::set<Observer*>::iterator it; for(it = observers.begin(); it != observers.end(); it++) (*it)->update(this, arg); } }; #endif // OBSERVABLE_H ///:~ Again, the design here is more elaborate than is necessary; as long as there’s a way to register an Observer with an Observable and for the Observable to update its Observer s, the set of member functions doesn’t matter. However, this design is intended to be reusable (it was lifted from the design used in the Java standard library). As mentioned elsewhere in the book, there is no support for multithreading in the Standard C++ libraries, so this design would need to be modified in a multithreaded environment. Observable has a flag to indicate whether it’s been changed. In a simpler design, there would be no flag; if something happened, everyone would be notified. The flag allows you to wait, and only notify the Observer s when you decide the time is right. Notice, however, that the control of the flag’s state is protected , so that only an inheritor can decide what constitutes a change, and not the end user of the resulting derived Observer class. The collection of Observer objects is kept in a set<Observer*> to prevent duplicates; the set insert( ) , erase( ) , clear( ) and size( ) functions are exposed to allow Observer s to be added and removed at any time, thus providing runtime flexibility. Most of the work is done in notifyObservers( ) . If the changed flag has not been set, this does nothing. Otherwise, it first clears the changed flag so repeated calls to notifyObservers( ) won’t waste time. This is done before notifying the observers in case the calls to update( ) do anything that causes a change back to this Observable object. Then it moves through the set and calls back to the update( ) member function of each Observer . At first it may appear that you can use an ordinary Observable object to manage the updates. But this doesn’t work; to get an effect, you must inherit from Observable and somewhere in your derived-class code call setChanged( ) . This is the member function that sets the “changed” flag, which means that when you call notifyObservers( ) all of the observers will, in fact, get notified. Where you call setChanged( ) depends on the logic of your program. Now we encounter a dilemma. An object that should notify its observers about things that happen to it – events or changes in state – might have more than one such item of interest. For example, if you’re dealing with a graphical user interface (GUI) item – a button, say – the items of interest might be the mouse clicked the button, the mouse moved over the button, and Chapter 16: Design Patterns 453 (for some reason) the button changed its color. So we’d like to be able to report all of these events to different observers, each of which is interested in a different type of event. The problem is that we would normally reach for multiple inheritance in such a situation: “I’ll inherit from Observable to deal with mouse clicks, and I’ll … er … inherit from Observable to deal with mouse-overs, and, well, … hmm, that doesn’t work.” The “interface” idiom The “inner class” idiom Here’s a situation where we do actually need to (in effect) upcast to more than one type, but in this case we need to provide several different implementations of the same base type. The solution is something I’ve lifted from Java, which takes C++’s nested class one step further. Java has a built-in feature called inner classes , which look like C++’s nested classes, but they do two other things: 1. A Java inner class automatically has access to the private elements of the class it is nested within. 2. An object of a Java inner class automatically grabs the “this” to the outer class object it was created within. In Java, the “outer this” is implicitly dereferenced whenever you name an element of the outer class. [[ Insert the definition of a closure ]]. So to implement the inner class idiom in C++, we must do these things by hand. Here’s an example: //: C09:InnerClassIdiom.cpp // Example of the "inner class" idiom #include <iostream> #include <string> using namespace std; class Poingable { public: virtual void poing() = 0; }; void callPoing(Poingable& p) { p.poing(); } class Bingable { public: virtual void bing() = 0; }; void callBing(Bingable& b) { Chapter 16: Design Patterns 454 b.bing(); } class Outer { string name; // Define one inner class: class Inner1; friend class Outer::Inner1; class Inner1 : public Poingable { Outer* parent; public: Inner1(Outer* p) : parent(p) {} void poing() { cout << "poing called for " << parent->name << endl; // Accesses data in the outer 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 << "bing called for " << parent->name << endl; } } inner2; public: Outer(const string& nm) : name(nm), inner1(this), inner2(this) {} // Return reference to interfaces // implemented by the inner classes: operator Poingable&() { return inner1; } operator Bingable&() { return inner2; } }; int main() { Outer x("Ping Pong"); // Like upcasting to multiple base types!: callPoing(x); callBing(x); Chapter 16: Design Patterns 455 } ///:~ The example begins with the Poingable and Bingable interfaces, each of which contain a single member function. The services provided by callPoing( ) and callBing( ) require that the object they receive implement the Poingable and Bingable interfaces, respectively, but they put no other requirements on that object so as to maximize the flexibility of using callPoing( ) and callBing( ) . Note the lack of virtual destructors in either interface – the intent is that you never perform object destruction via the interface. Outer contains some private data ( name ) and it wishes to provide both a Poingable interface and a Bingable interface so it can be used with callPoing( ) and callBing( ) . Of course, in this situation we could simply use multiple inheritance. This example is just intended to show the simplest syntax for the idiom; we’ll see a real use shortly. To provide a Poingable object without inheriting Outer from Poingable , the inner class idiom is used. First, the declaration class Inner says that, somewhere, there is a nested class of this name. This allows the friend declaration for the class, which follows. Finally, now that the nested class has been granted access to all the private elements of Outer , the class can be defined. Notice that it keeps a pointer to the Outer which created it, and this pointer must be initialized in the constructor. Finally, the poing( ) function from Poingable is implemented. The same process occurs for the second inner class which implements Bingable . Each inner class has a single private instance created, which is initialized in the Outer constructor. By creating the member objects and returning references to them, issues of object lifetime are eliminated. Notice that both inner class definitions are private , and in fact the client programmer doesn’t have any access to details of the implementation, since the two access methods operator Poingable&( ) and operator Bingable&( ) only return a reference to the upcast interface, not to the object that implements it. In fact, since the two inner classes are private , the client programmer cannot even downcast to the implementation classes, thus providing complete isolation between interface and implementation. Just to push a point, I’ve taken the extra liberty here of defining the automatic type conversion operators operator Poingable&( ) and operator Bingable&( ) . In main( ) , you can see that these actually allow a syntax that looks like Outer is multiply inherited from Poingable and Bingable . The difference is that the casts in this case are one way. You can get the effect of an upcast to Poingable or Bingable , but you cannot downcast back to an Outer . In the following example of observer, you’ll see the more typical approach: you provide access to the inner class objects using ordinary member functions, not automatic type conversion operations. The observer example Armed with the Observer and Observable header files and the inner class idiom, we can look at an example of the observer pattern: //: C09:ObservedFlower.cpp // Demonstration of "observer" pattern #include "Observable.h" #include <iostream> #include <vector> Chapter 16: Design Patterns 456 #include <algorithm> #include <string> using namespace std; class Flower { bool isOpen; public: Flower() : isOpen(false), openNotifier(this), closeNotifier(this) {} void open() { // Opens its petals isOpen = true; openNotifier.notifyObservers(); closeNotifier.open(); } void close() { // Closes its petals isOpen = false; closeNotifier.notifyObservers(); openNotifier.close(); } // Using the "inner class" idiom: class OpenNotifier; friend class Flower::OpenNotifier; class OpenNotifier : public Observable { Flower* parent; bool alreadyOpen; public: OpenNotifier(Flower* f) : parent(f), alreadyOpen(false) {} void notifyObservers(Argument* arg=0) { if(parent->isOpen && !alreadyOpen) { setChanged(); Observable::notifyObservers(); alreadyOpen = true; } } void close() { alreadyOpen = false; } } openNotifier; class CloseNotifier; friend class Flower::CloseNotifier; class CloseNotifier : public Observable { Flower* parent; bool alreadyClosed; public: CloseNotifier(Flower* f) : parent(f), Chapter 16: Design Patterns 457 alreadyClosed(false) {} void notifyObservers(Argument* arg=0) { if(!parent->isOpen && !alreadyClosed) { setChanged(); Observable::notifyObservers(); alreadyClosed = true; } } void open() { alreadyClosed = false; } } closeNotifier; }; class Bee { string name; // An "inner class" for observing openings: class OpenObserver; friend class Bee::OpenObserver; class OpenObserver : public Observer { Bee* parent; public: OpenObserver(Bee* b) : parent(b) {} void update(Observable*, Argument *) { cout << "Bee " << parent->name << "'s breakfast time!\n"; } } openObsrv; // Another "inner class" for closings: class CloseObserver; friend class Bee::CloseObserver; class CloseObserver : public Observer { Bee* parent; public: CloseObserver(Bee* b) : parent(b) {} void update(Observable*, Argument *) { cout << "Bee " << parent->name << "'s bed time!\n"; } } closeObsrv; public: Bee(string nm) : name(nm), openObsrv(this), closeObsrv(this) {} Observer& openObserver() { return openObsrv; } Observer& closeObserver() { return closeObsrv;} }; Chapter 16: Design Patterns 458 class Hummingbird { string name; class OpenObserver; friend class Hummingbird::OpenObserver; class OpenObserver : public Observer { Hummingbird* parent; public: OpenObserver(Hummingbird* h) : parent(h) {} void update(Observable*, Argument *) { cout << "Hummingbird " << parent->name << "'s breakfast time!\n"; } } openObsrv; class CloseObserver; friend class Hummingbird::CloseObserver; class CloseObserver : public Observer { Hummingbird* parent; public: CloseObserver(Hummingbird* h) : parent(h) {} void update(Observable*, Argument *) { cout << "Hummingbird " << parent->name << "'s bed time!\n"; } } closeObsrv; public: Hummingbird(string nm) : name(nm), openObsrv(this), closeObsrv(this) {} Observer& openObserver() { return openObsrv; } Observer& closeObserver() { return closeObsrv;} }; int main() { Flower f; Bee ba("A"), bb("B"); Hummingbird ha("A"), hb("B"); f.openNotifier.addObserver(ha.openObserver()); f.openNotifier.addObserver(hb.openObserver()); f.openNotifier.addObserver(ba.openObserver()); f.openNotifier.addObserver(bb.openObserver()); f.closeNotifier.addObserver(ha.closeObserver()); f.closeNotifier.addObserver(hb.closeObserver()); f.closeNotifier.addObserver(ba.closeObserver()); f.closeNotifier.addObserver(bb.closeObserver()); Chapter 16: Design Patterns 459 // Hummingbird B decides to sleep in: f.openNotifier.deleteObserver(hb.openObserver()); // Something changes that interests observers: f.open(); f.open(); // It's already open, no change. // Bee A doesn't want to go to bed: f.closeNotifier.deleteObserver( ba.closeObserver()); f.close(); f.close(); // It's already closed; no change f.openNotifier.deleteObservers(); f.open(); f.close(); } ///:~ The events of interest are that a Flower can open or close. Because of the use of the inner class idiom, both these events can be separately-observable phenomena. OpenNotifier and CloseNotifier both inherit Observable , so they have access to setChanged( ) and can be handed to anything that needs an Observable . You’ll notice that, contrary to InnerClassIdiom.cpp , the Observable descendants are public . This is because some of their member functions must be available to the client programmer. There’s nothing that says that an inner class must be private ; in InnerClassIdiom.cpp I was simply following the design guideline “make things as private as possible.” You could make the classes private and expose the appropriate methods by proxy in Flower , but it wouldn’t gain much. The inner class idiom also comes in handy to define more than one kind of Observer , in Bee and Hummingbird , since both those classes may want to independently observe Flower openings and closings. Notice how the inner class idiom provides something that has most of the benefits of inheritance (the ability to access the private data in the outer class, for example) without the same restrictions. In main( ) , you can see one of the prime benefits of the observer pattern: the ability to change behavior at runtime by dynamically registering and un-registering Observer s with Observable s. If you study the code above you’ll see that OpenNotifier and CloseNotifier use the basic Observable interface. This means that you could inherit other completely different Observer classes; the only connection the Observer s have with Flower s is the Observer interface. Multiple dispatching When dealing with multiple types which are interacting, a program can get particularly messy. For example, consider a system that parses and executes mathematical expressions. You want to be able to say Number + Number , Number * Number , etc., where Number is the base class for a family of numerical objects. But when you say a + b , and you don’t know the exact type of either a or b , so how can you get them to interact properly? Chapter 16: Design Patterns 460 The answer starts with something you probably don’t think about: C++ performs only single dispatching. That is, if you are performing an operation on more than one object whose type is unknown, C++ can invoke the dynamic binding mechanism on only one of those types. This doesn’t solve the problem, so you end up detecting some types manually and effectively producing your own dynamic binding behavior. The solution is called multiple dispatching . Remember that polymorphism can occur only via member function calls, so if you want double dispatching to occur, there must be two member function calls: the first to determine the first unknown type, and the second to determine the second unknown type. With multiple dispatching, you must have a virtual call to determine each of the types. Generally, you’ll set up a configuration such that a single member function call produces more than one dynamic member function call and thus determines more than one type in the process. To get this effect, you need to work with more than one virtual function: you’ll need a virtual function call for each dispatch. The virtual functions in the following example are called compete( ) and eval( ) , and are both members of the same type. (In this case there will be only two dispatches, which is referred to as double dispatching ). If you are working with two different type hierarchies that are interacting, then you’ll have to have a virtual call in each hierarchy. Here’s an example of multiple dispatching: //: C09:PaperScissorsRock.cpp // Demonstration of multiple dispatching #include " /purge.h" #include <iostream> #include <vector> #include <algorithm> #include <cstdlib> #include <ctime> using namespace std; class Paper; class Scissors; class Rock; enum Outcome { win, lose, draw }; ostream& operator<<(ostream& os, const Outcome out) { switch(out) { default: case win: return os << "win"; case lose: return os << "lose"; case draw: return os << "draw"; } } [...]... fillBin() #include "fillBin.h" #include "Fillable.h" #include " /C01/trim.h" #include " /require.h" #include #include #include 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(','); Chapter 16: Design Patterns 484 // Parse each line into entries:... #include #include void fillBin(std::string filename, Fillable& bin); // Special case to handle vector: inline void fillBin(std::string filename, std::vector& bin) { Fillablevector fv(bin); fillBin(filename, fv); } #endif // FILLBIN_H ///:~ The overloaded version will be discussed shortly First, here is the implementation: //: C09:fillBin.cpp {O} // Implementation of fillBin()... { 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(rand() % maxWeight)); } }; int main() { vector bin; // Fill up the Trash bin: InfoGen infoGen(3, 100); for(int i = 0; i < 30; i++) bin.push_back(Trash::factory(infoGen()));... 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: //: C09:fillBin.h // Open a file and parse its contents into // Trash objects, placing each into a vector #ifndef FILLBIN_H #define FILLBIN_H #include "Fillablevector.h"... 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 //: C09: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)... 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... 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& 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... 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: //: C09:Recycle3.cpp //{L} TrashPrototypeInit //{L} fillBin Trash TrashStatics // Recycling with RTTI and Prototypes #include "Trash.h" #include... TrashStatics // Recycling with RTTI and Prototypes #include "Trash.h" #include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "fillBin.h" #include "sumValue.h" #include " /purge.h" #include #include using namespace std; ofstream out("Recycle3.out"); int main() { vector bin; // Fill up the Trash bin: Chapter 16: Design Patterns 486 ... 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, . "inner class" idiom #include <iostream> #include <string> using namespace std; class Poingable { public: virtual void poing() = 0; }; void callPoing(Poingable&. return inner1; } operator Bingable&() { return inner2; } }; int main() { Outer x("Ping Pong"); // Like upcasting to multiple base types!: callPoing(x); callBing(x);. methods by proxy in Flower , but it wouldn’t gain much. The inner class idiom also comes in handy to define more than one kind of Observer , in Bee and Hummingbird , since both those classes

Ngày đăng: 05/07/2014, 19:20