This guide covers the other half of Phoenix's coding convention. Analogous to the syntactic guidelines established in the Phoenix C++ Coding Specification this document covers the design conventions to follow and, in addition, shows how to use the framework support provided (as described in Phoenix Framework Guide) to produce new components which follow this convention.
What's described here is a methodology for designing objects within Phoenix. Beyond naming conventions and a list of guidelines for basic source code consistency, we want conventions and guidelines for the high level designs of objects as well. We'll define a convention for all interfaces used in Phoenix and then continue on to describe how interfaces for components should be designed and how these objects are integrated with the Phoenix framework.
As we get into higher-level topics, much of the methodology is described by example. There's quite a bit of discussion that goes along with them that is intended to show why it's useful to design things this way and to help you think about how to use this methodology when faced with your own unique problems.
An attribute-only interface is an interface to an object that consists only of reading and writing attributes. For instance:
aircraft->landingGearState(RETRACTING); // the landing gear state attribute of aircraft is now "retracting" ... if (aircraft->landingGearState() == RETRACTING) { // update landing gear }
There are some basic assumptions behind these accesses.
1. Read accesses are Exception free. They don't throw exceptions.
2. Read accesses have the Nilpotence property. They don't change the logical state of the object (i.e., have no externally visible side-effects, though things could change internally).
3. Write accesses have the Idempotence property. Writing the same attribute twice with the same value is the same as writing it once. This seems strangely hard to ensure sometimes; I wouldn't be a stickler for this, but do your best. Documentation on write accessors is critical for reasons like this, and it's important that you understand what a write accessor does when you call it. Most of the time it'll be clear, but just keep it in mind.
4. Write accesses are Transactional. This means "all or nothing", essentially. If writing an attribute fails, then the object should be left alone -- it can't be left in a "half-way" state. So if you're busy changing a bunch of values and something fails, you should revert the object to its previous state. This is usually pretty easy if the operation is simple, but can be hard sometimes. If the state can't be repaired (maybe you just deleted something and can't get it back), then this is where we should consider throwing a fatal exception because we've broken our contract with the client, and they may not be able to deal with this or even know about it.
The effect of these assumptions is to make attributes look like raw values that we're just setting and getting via the interface methods. When we do:
aircraft->landingGearState(EXTENDING);
As you can see, attribute names are always nouns (See Attribute names). A verb isn't exactly something that we can view the same way -- so just don't use them. Sometimes it requires a great deal of cleverness to design an interface this way. An example presented by Cheriton (see Acknowledgements):
collection->sort(); // bad; sort() is not an attribute. assert(collection->isSorted()); collection->order(INCREASING); // good; order() is an attribute // This has the effect of sorting the collection in increasing order. assert(collection->order() == INCREASING);
I won't claim that the latter is better from a readability standpoint, or in any way "prettier". The important quality is that it's consistent with other objects. Our collection object interface works like the interface to our aircraft; the only difference is that they have different attributes.
// read access Wheel* wheel1 = landingGear->wheel(1); // write access landingGear->wheel(1, wheel1);
The first parameter is the index, or "selector", that tells which wheel in the "wheel" collection we're writing. For a read accessor, this is the only parameter, and the method returns the wheel in question (or maybe a NULL pointer if there is no such wheel -- remember, no exception throwing!) The write accessor has a second parameter which is the new value to write for the wheel. We can look at this as a simple convenience (an optimization) over doing the following:
// read access Wheel* wheel1 = landingGear->wheel1(); // write access landingGear->wheel1(wheel1);
So, our collection attribute above is just a way to refer to a bunch of attributes called wheel1, wheel2, etc. but without explicitly making a new attribute for each one (which would also limit us to a fixed number of wheels). In general, you can use anything for a selector -- not just an integer as in this case.
We could use the above syntax to do this. In that situation, we just use the object itself as the selector:
// write access -- add aWheel to the "wheel" collection attribute. Wheel aWheel; // put pointer to aWheel into collection, keyed by the pointer itself. landingGear->wheel(&aWheel, &aWheel); ... if (landingGear->wheel(&aWheel)) { // aWheel is in the collection represented by the 'wheel' attribute. } else { // If the read accessor returns NULL, then aWheel is not a valid selector. // This means that aWheel is not in the collection. } // write access -- remove aWheel from the "wheel" collection attribute // by writing NULL in the place keyed by the pointer. landingGear->wheel(&aWheel, 0); //Note that the read accessor above now returns NULL; which is exactly //the value we just set for the attribute here. Thus, writing null is //like saying, "this element is not in the set". Internally, //landingGear doesn't have to actually store a NULL pointer to record //this information -- it can discard aWheel if it wants. The //important aspect is that the client doesn't need to know whether //this is the case; it all "looks" just like reading/writing attribute //values.
Obviously, this is a bit unwieldy and maybe a tad confusing for people. So, we introduce some more short-hand for this situation:
landingGear->newWheel(&aWheel); // adds a new wheel (write access); if (landingGear->wheel(&aWheel)) { // same syntax, same meaning } landingGear->deleteWheel(&aWheel); // removes a wheel (write access);
So, newWheel (in this context) is just a way to add a wheel to the collection by writing its pointer into the collection, keyed by the pointer itself. deleteWheel just writes NULL as shown above. Again, logically, we're still reading/writing attributes; we've just changed the syntax.
class Planet { void newCreature(void) { mCreatures++; } void deleteCreature(void) { mCreatures--; } uint32_t creatureCount(void) const { return mCreatureCounter; } };
Here, we don't actually care to create and store all of the creatures on our planet -- we just want to be able to add and remove them and keep track of how many there are. The Ptr class has newReference() and deleteReference() that keep track of how many pointers refer to an object (rather than keeping a list of all the pointers pointing to the object).
When referring to the number of items in a collection, please use the collection name followed by 'Count', as opposed to numOfCreatures(), or creatures() for the above example.
Bijections are another kind of collection that maps keys to elements, as usual, but also prevents an element from appearing twice with different keys. I.e., each key maps to one element, and each element maps back to one key. In such cases, the standard "new" notation works with the additional hitch that attempting to add a new key-value pair that would violate the bijection (because value is already mapped to another key), causes an InUseException to be thrown. A standard naming pattern for such collections is to use key and value names as the attribute name, and invert the name for reverse lookup. E.g.:
void computerSerialNumber(Computer c, SerialNumber s); // write SerialNumber computerSerialNumber(Computer c); // read Computer serialNumberComputer(SerialNumber s); // reverse-lookup
This inversion often is less aesthetic than its original counterpart, but the names are clearly bound together.
Other collections are possible, and notational abuse is possible though it can frequently be worked around. A stack-style collection attribute can be implemented as shown below.
str.character(str.size(), c); // push char 'c' to end of string // str is grown to fit 'c'; size() is incremented as a side effect. str.character(str.size()-1, 0); // pop char from end of buffer // str notes that the string has been truncated by a null character // and reduces the size() parameter appropriately.
Often, however, this is impractical and some additional fudging is warranted:
str.newCharacter(str.size(), c); // insert 'c' at end str.deleteCharacter(str.size()-1); // delete 'c' from end
As always, spend some time thinking, and use increasingly perverted forms of notation only as absolutely necessary -- when all more standardized versions are clearly less understandable.
When we want to create a new object we want to avoid, wherever possible, having the client use the "new" operator. This means that the client is instantiating a type of object whose complete type is known at compile time (if the complete type were not known, the compiler would not know how much memory to allocate). Generally this is not what we want. We want an architecture where it's safe to completely swap implementations, even at runtime. For instance:
// Bad; have to compile against Clock implementation. Clock* clock = new Clock(); // Better; only compiling against the Clock @e interface // which is implemented by some hidden object. Clock* clock = manager->newClock();
As you can see, the solution is to encapsulate the creation of objects inside a manager that knows about the objects. As new object types get added, we can update the manager. More importantly, if the implementation of Clock changes, the client does not need to know and, in fact, does not even need to be recompiled. Since Phoenix uses shared libraries to store a lot of its code, this is very useful -- we can update the implementations in our shared libraries and clients outside of the library do not need to be recompiled unless the public interface changes.
The only reason I bring this up here is that this is a heavily perverted application of the syntax presented above -- we're overloading the notation. Just remember that when you see newAircraft(&aircraft) or something of the sort, it's a collection as described earlier. If it returns something, and has different parameters, it's creating a new element of a collection like the above example. This is a minor inconvenience, but I think you'll find it's not so bad. When in doubt, check the docs.
A "Value" is an object that represents exactly that -- a value. This may be a scalar quantity, or it could be a complex multi-valued structure. The identifying characteristic is that it make sense for values to be compared with == and to be assigned to each other (either via operator= or by copy construction). Because of this, values have no "identity". If two values compare as equal, then they are completely equivalent and may be used interchangably.
An "Entity", on the other hand, is a type of object that represents a "thing" in our simulation. Every real-world system that you're simulating is going to be an entity. Most major systems within the simulator (modeled after the real world or not) are entities. These objects contrast with value-types in that it doesn't make sense to compare them to each other or copy them. Entities are the parts of the system that actually simulate the real world -- value types, on the other hand, are just computational components that help entities do their jobs.
If this is not otherwise clear, check out the Entity Classes and Value-type Classes sections for a similar description along with some examples of classes that we would expect to be entities and value types.
When it comes to value types, life is pretty simple. We follow the attribute-only interface, and just implement things as usual with methods in the class definition being implemented inline or in the accompanying .cpp file. Entities are a different proposition. These objects have much bigger jobs to do. Not only should entities follow the attribute-only interface policy, but they should be designed following the approach described below which rigorously separates the entity's public interface and describes how to organize functionality into multiple interfaces.
Those who've used Java will find the following method similar to Java's built-in "interface" support.
// myInterface.h class MyInterface { public: virtual void publicInterfaceAttribute(int a) = 0; virtual int publicInterfaceAttribute(void) = 0; }; // myInterface.cpp class MyInterfaceImpl : public MyInterface { // adaptation of public interface void publicInterfaceAttribute(int a) { uglyMethod1(a); uglyMethod2(a); } int publicInterfaceAttribute(void) { return complicatedComputation() + 1; } // private methods void uglyMethod1(int a) { ... } void uglyMethod2(int a) { ... } int complicatedComputation(void) { ... } // private data structures };
The above example, in terms of code, creates a subclass of the interface (MyInterface) where the subclass implements the interface methods to provide the necessary functionality. Alternatively, think of the implementation as a separate, hidden object. The implementations of the virtual functions "adapt" the internal object's private interface to the exposed public interface. MyInterface does not contain any implementation code or data; it only describes the interface to the (invisible) implementation.
C++ separates implementation and interface naturally by letting you implement your class methods in separate files from their declarations in a header. However, this often means that you put all of your private methods, private data structures, header inclusions, etc. in the headers as well, which exposes gory details to the client and also prevents us from easily swapping in new implementations behind the client's back (because new implementations would bring new headers with them). The method shown above goes farther and completely divorces interface and implementation; you should follow this pattern whenever you implement an entity (which will be most of the code you write) or whenever the complexity of your object warrants.
class Tank { public: uint32_t length(void) const { return mLength; } protected: void setLength(uint32_t length) { mLength = length; } private: uint32_t mLength; };
Note that the implementor cannot override length() now because it is not virtual, but the client will be able to access the length value quickly. The implementor needs a way to adjust this value from within the private code. For this, the interface class should provide a protected inline set...() method (in this case setLength()). The implementation will just use this method to adjust the attribute value as necessary.
All of that said, you will often have, as in this case, some implementation in your interfaces. But remember that doing so makes a strong statement: that every possible implementation of the interface will use this code. For an inline accessor as above, this is perfectly fine. However, the following is questionable:
class Aircraft { public: double radarCrossSection(void) { // crazy math code... } private: void setRCSValue1(..); void setRCSValue2(..); // private data for RCS computation };
This code assumes that every implementation will want to use the same computational model for radarCrossSection() with the same parameters -- this is probably not a safe assumption. In this case, the method is complex enough that the cost of computing it is probably more than the cost of making it virtual. You will have to decide where to draw the line for yourself.
There is an additional reason for doing this: an object which has just one interface but multiple clients using it has a problem. The clients may attempt accesses at any time and may be doing things that other clients don't know about. Your single interface then would have to define exactly what happens during these periods of contention. If a client attempts to set two attributes, is it guaranteed that another client can't change one of them in between? And if so, how do we implement this? Even if you can do this, it will quickly become hard to keep track of for the implementor -- and worse, clients may need to be explicitly aware of what other clients are doing. In general, we not only want to divide up functionality between interfaces, but we also want to provide one interface for every client. In this way, we insulate clients from each other, giving them only the services they need in a form that keeps them safely separated from other clients.
Your object will have a top-level interface that has one client. This interface's job is to provide functionality needed by the "primary" client (the one that created the object, usually), and then allow "sub interfaces" to be created which offer safe, independent access to the object and also specialize interfaces for clients that only want a certain kind of service. This is where the perspective of an interface "adapting" an object to a client's needs becomes more important. We will now have multiple clients, each with their own personal interface instance, and each of these will adapt the behavior of the same object to their needs.
Below is an interface for a hypothetical Window that allows multiple clients to draw shapes (circles and rectangles):
#include <Phx/PhxConfig.h> #include <Phx/PhxTypes.h> // For Ptr and LockedPtrInterface class Window : LockedPtrInterface<Window> { public: class PaintInterface; // sub interfaces virtual Ptr<PaintInterface> newPaintInterface(void) = 0; // attributes for the primary client virtual void title(const String& title) = 0; }; class Window::PaintInterface : public LockedPtrInterface<PaintInterface> { public: // attributes for a painting client virtual void penPosition(const Point2D& penPosition) = 0; virtual void fillColor(const Color& fillColor) = 0; virtual void lineColor(const Color& lineColor) = 0; virtual void newCircle(double radius) = 0; virtual void newRectangle(double width, double height) = 0; };
Mentally, you can picture an instance of Window as providing access to a hidden implementation object. One of the things that this interface can do is give you a pointer to a new interface that accesses the same hidden object. However, the new interface may provide additional functionality and, most importantly, it can be used by another client independently of the operation of all the other interfaces. In this case, Window provides a PaintInterface subinterface. Each client can get one instance, and all of them issue paint commands to the same hidden object.
Here's a standard way to implement this (though you can certainly come up with variations):
class WindowImpl : public Window { public: class PaintInterfaceImpl; // sub interfaces Ptr<PaintInterface> newPaintInterface(void); // primary client's attributes.. void title(const String& title); // support for drawing // draw circle at cx,cy with radius r and given color void paintCircle(double cx, double cy, double r, const Color& fillColor, const Color& lineColor); // draw rectangle in region (x1,y1)-(x2, y2) with given colors void paintRectangle(double x1, double y1, double x2, double y2, const Color& fillColor, const Color& lineColor); private: String mTitle; }; class WindowImpl::PaintInterfaceImpl : public Window::PaintInterface { public: // constructs a new sub interface attached to 'owner' PaintInterfaceImpl(const Ptr<WindowImpl>& owner) : mWindow(owner) {} // paint client's attributes void penPosition(const Point2D& penPosition) { mPenPos = penPosition; } void fillColor(const Color& fillColor) { mFillColor = fillColor; } void lineColor(const Color& lineColor) { mLineColor = lineColor; } void newCircle(double radius) { mWindow->paintCircle(mPenPos.x(), mPenPos.y(), radius, mFillColor, mLineColor); } void newRectangle(double width, double height) { mWindow->paintRectangle(mPenPos.x(), mPenPos.y(), mPenPos.x() + width, mPenPos.y() + height, mFillColor, mLineColor); } private: Color mFillColor, mLineColor; Point2D mPenPos; Ptr<WindowImpl> mWindow; }; Ptr<Window::PaintInterface> WindowImpl::newPaintInterface(void) { // just give client a new PaintInterface implementation // that is attached to this return new PaintInterfaceImpl(Ptr<WindowImpl>(this)); }
Some things to notice about this example:
First, each client can create its own PaintInterface attached to the same Window instance. They'll all need to call newPaintInterface() on the original window object, but this method is very simple (it just returns a new subinterface instance) and safe to use, even concurrently. In general: methods that create subinterfaces, by nature, must support some form of access, directly or indirectly, by multiple clients. This is a slight exception to the "one client per interface" policy, but as you can see this is not a problem because this operation is usually very simple to protect.
Also, note that in addition to the subinterfaces, the Window class provides a simple title() attribute. By our "one client per interface" assumption, we intend for this attribute to only be used by the client that the Window interface belongs to. It would be wrong for multiple clients to attempt to set the title() of the Window themselves -- we place this ability in the hands of the one and only client that should be using the Window interface (the "primary" client). Again, newPaintInterface() (and any method that creates subinterfaces) can be used by multiple clients because we don't have better options -- but other attributes are strictly off limits to all but the primary client for that interface. In general, singleton attributes (or singleton subinterfaces, which are attributes too) should never be used by multiple clients.
A better, though maybe slightly less convenient, solution is to not put any real attributes in Window. Instead, one could have an additional subinterface type for the primary client, e.g., WindowController which provided attributes for setting the title, etc. and then provide access to this interface via a method like controllerInterface(), whose name indicates that it's a singleton interface and not a collection of interfaces like newPaintInterface(). Under this design, the Window class just becomes a "dumb" access point that distributes subinterfaces to the clients. A nice aspect of this is that all of the methods are accessible by multiple clients except for the one singleton attribute (which, obviously, can only support one client -- assumed to be the primary client) and there is less chance for confusion.
When we have a bunch of clients that want to paint, each may get its own PaintInterface leading back to the target window. Clients can independently set their drawing attributes (like fill color and pen position) on their interface instance without upsetting each other. When they call newRectangle(..) on their PaintInterface to add a new painted rectangle to the window, their current settings are used to paint the rectangle in the window. Notice that if we assume only one client accesses a PaintInterface, then we don't need locking for the attributes. They're only supposed to be accessed by that interface's single client from a single thread at a time -- this is our contract with the client. Because of this assumption, we can work safely within the subinterface without worries.
An implementation note: because we use a smart-pointer to link our subinterface back to the Window, the window will not be destroyed until every client has released the reference to its subinterface (and thereby release the internal reference to the target window). From an interface standpoint this may be good or bad, but in any event you can choose to keep raw pointers instead. Either way, you will need to have a scheme for clients to be notified when the Window must be torn down. [One solution is to add a listener() attribute to PaintInterface whose Listener object provides an onClose() hook which will be invoked when the owning Window closes.] Regardless of your solution, be sure that the client is reasonably well insulated. Because we use a smart pointer above, if the window is closed it can be taken off the screen and its graphics buffer freed but its Window object left in memory, dangling from the remaining smart pointers belonging to the clients. These clients could happily draw until they were finished (and the Window could just eat the drawCircle/Rectangle calls). Eventually they will finish and throw away their pointers, freeing the Window structure. This is very desirable since somebody could close the window while a client is in the middle of drawing. Even if you notify the client, it may be stuck in the middle of its drawing code (on another thread) and won't be able to detach itself from the window (by dropping is smart-pointer reference) until later. But since the paintCircle/Rectangle() calls work even after the Window is closed, everything is safe and the client can terminate at its leisure. Alternatively, an implementation using raw pointers could loop through a list of subinterfaces for the Window and set their mWindow pointers to 0. The subinterfaces could then just ignore newRectangle/newCircle calls. All of this is up to you, but try to do something reasonable so that situations like this (where the window closes and the client is still trying to draw) don't lead to exceptions and crashes.
Now, one place where threading becomes an issue is the call to WindowImpl::paintCircle/Rectangle(). Because these are a part of the original Window, multiple clients may call them at once -- so they must be thread-safe. While we may have to provide locking, we can provide it easily because this is all hidden away in our implementation. Consider what might have happened if we attempted to provide the same interface as part of the single Window class without using a PaintInterface subinterface:
class Window : public LockedPtrInterface<Window> { public: // attribute for the primary client // attributes for all of the painting clients virtual void penPosition(const Point2D& penPosition) = 0; virtual void fillColor(const Color& fillColor) = 0; virtual void lineColor(const Color& lineColor) = 0; virtual void newCircle(double radius) = 0; virtual void newRectangle(double width, double height) = 0; }; ... // client1 code window->penPosition(Point2D(10, 10)); window->lineColor(Color::green()); window->fillColor(Color::red()); window->newCircle(1.0); // Expecting a radius 1.0 circle to be drawn at 10,10 with a red fill // and green line color. ... // client 2 code window->penPosition(Point2D(0, 0)); window->fillColor(Color::blue()); window->newCircle(1.0); // Expecting a radius 1.0 circle to be drawn at 0,0 with a blue fill // and default line color.
First, let's not even consider what might happen in a threaded environment and stick to the single-threaded situation. Suppose that client1 runs, then client 2 runs later. Will client 2 get what it expects? No. It was expecting a default line color (let's say Color::black()), but ended up drawing a blue circle with a green outline because client 1 set the color to green but didn't set it back to the default that client 2 was expecting. So, either client 1 must be trusted to undo its state changes each time, or client 2 must know about client 1's behavior - barring this, client 2 has to reset all of the attributes it cares about to defaults every time. Anyone that has used a state-based graphics API (e.g., OpenGL) knows how frustrating this can be -- somebody has turned off depth buffering, and now everyone has to turn it back on every time they draw unless the culprit is located. Arguably, this can be solved by disciplined convention and using various APIs to save and restore the state.
In a threaded environment things are much worse. If client 1 and client 2 run concurrently, then their calls may become interlaced (even if the calls themselves are thread-safe). One trace might show:
window->penPosition(Point2D(10, 10)); // client 1 window->lineColor(Color::green()); // client 1 window->fillColor(Color::red()); // client 1 window->penPosition(Point2D(0, 0)); // client 2 window->fillColor(Color::blue()); // client 2 window->newCircle(1.0); // client 2 window->newCircle(1.0); // client 1
Now both clients will draw circles at 0,0 with blue fill and green outline -- clearly not what either wanted. One solution is to allow the clients to lock the window when they wish to draw or modify an attribute. However, clients must be careful with this; in general, relying on the client to perform locking is something to avoid. Having a separate interface for each client is a way to insulate good-behaving clients from clients that behave poorly -- each client gets its own sand-box on top of the same bed of sand.
A final academic point: someone has already pointed out that this approach looks a lot like the Model-Viewer-Controller design pattern. Certainly MVC-style designs are right at home within this methodology so feel free to think of things this way if it helps for your object(s). You could implement these with the "model" as the top-level interface to your object. The "viewer" and "controller" interfaces could then be subinterfaces supporting clients that wish to view/control the model data, respectively. An Input class, for example, could basically be of this design -- an Input layer represents the model, clients receive events via subinterfaces (the "viewers") and input devices (joysticks, mice, keyboards -- "controllers") provide data input to the system via another subinterface.
// in WindowImpl
std::vector<Ptr<PaintInterface> > mPaintInterfaces;
Unfortunately, this may introduce a circular reference -- in which case PaintInterface must use a raw pointer to refer to its owning window (rather than a smart-pointer as above). Another issue is that when a client releases their pointer, a reference to the interface will still be held by the WindowImpl object itself, and thus the interface will not be destroyed until the WindowImpl is destroyed. This may be undesired. One way to solve the problem is to require that the client explicitly remove its interface via a corresponding delete method:
: public LockedPtrInterface<Window> { ... virtual void deletePaintInterface(const Ptr<PaintInterface>& paintInterface) = 0; ... };
The implementation of this method would remove the interface from the internal list. This approach does have two problems. The first is that we're modifying the interface to make room for a specific implementation. An implementation that did not need to keep track of the subinterfaces would implement this method with an empty body. The second, more practical, problem is that clients must remember to delete their interface explicitly, or else it will be left in the list and not freed. While this may not lead to a particularly harmful memory leak (the memory is still going to be freed when the window is destroyed), it does waste memory and could cause problems if it happens too frequently. Nonetheless, in some situations this kind of interface is the only reasonable solution and deals with the problem adequately.
A better solution is to try to hide this operation from the client. The client expects that when they release all of their pointers, the subinterface will be freed and it will detach from the Window gracefully. You can do this in your subinterface's destructor:
PaintInterfaceImpl::~PaintInterfaceImpl() {
// tell our window that we've been destroyed
mWindow->deletePaintInterface(this);
...
}
class WindowImpl : public Window {
...
void deletePaintInterface(PaintInterface* paintInterface) {
// find paintInterface in mPaintInterfaces and remove it.
}
...
std::vector<PaintInterface*> mPaintInterfaces;
}
WindowImpl provides a deletePaintInterface() method that removes the given pointer from its internal list (which now uses raw pointers, since the reference count would not reach zero if we used a smart-pointer). This method is not a part of the public interface. Essentially, we're doing exactly as in the previous solution, except that this time we're hiding the deletePaintInterface() method and calling it ourselves automatically when the PaintInterface gets destructed. We can do all the processing we want and the client does not need to cooperate.
The first step to doing this is to subclass Window from Phx::NamedInterface. This will bring the name(), identifier() and type() attributes with it, so clearly our Window class couldn't (without confusion) use these for itself -- no problem in this case. In addition, we need to choose a type constant which will identify our Window interface class. This type constant needs to be allocated by a central authority to assure uniqueness and consistency, which, for the purposes of this example, will be us. Let's use 100. Likewise, we need a unique type name which we'll choose to be "Window" (though the type name is optional).
So our declaration changes to:
// PhxWindow.h #include <Phx/PhxConfig.h> #include <Phx/PhxTypes.h> #include <Phx/Core/PhxNamedInterface.h> class Window : public NamedInterface { public: const static NamedInterface::Type INTERFACE_TYPE; const static String INTERFACE_TYPE_NAME; ... }; // PhxWindow.cpp const NamedInterface::Type Window::INTERFACE_TYPE(100); const String Window::INTERFACE_TYPE_NAME("Window");
So now we're subclassed from NamedInterface and we have a type constant defined within the code called Window::INTERFACE_TYPE whose value (which is not shown to the client) is 100. INTERFACE_TYPE is the standard name. We follow this convention so that interface type constants and names are easily found and are tied directly to their class definition (since the constant must be nested within the class). Moreover, the entity manager will let you define your type more easily if you stick to this convention. Likewise, INTERFACE_TYPE_NAME is the standard name and should always be defined for similar reasons. Because the name is optional, you don't have to choose a name. In this case, define the INTERFACE_TYPE_NAME value anyway and set its value to "".
Now we can define our interface type. The Core will have an interface manager (a NamedInterface::Manager instance) that keeps track of all of the existing instances of NamedInterfaces and their types. Anyone else that wants to create instances of our Window type will call up this manager and, using our type constant or name, request an instance of the Window interface. To allow this, we need to tell the manager about our type. We'll assume that you've arranged to have your code called when the program loads and have acquired a pointer from the Core to the the NamedInterface::Manager object. At that point, we have two ways to install our type: the easy way and the generic way.
First, the generic way. Use this when you need to pass your own type and name constants (because you didn't follow the convention), when you couldn't provide the necessary constructor needed for the easy way (shown below), or when you need to be able to explicitly track creation and deletion requests for your object. To allow the manager to generically call back to our code we use the standard framework support -- listeners. NamedInterface::Manager defines the NamedInterface::Manager::TypeListener listener class. We implement this listener's onNewNamedInterface and onDeleteNamedInterface methods to create and delete our object as necessary. Once we've done this, we can install that listener into the manager. In total, our new code would be:
// Define an appropriate listener class class WindowTypeListener : public NamedInterface::Manager::TypeListener { public: Ptr<NamedInterface> onNewNamedInterface(const Ptr<Description>& constructorData) { return new WindowImpl(); } }; // when your module loads... mMyWindowTypeListener = Ptr<WindowTypeListener>(new WindowTypeListener()); interfaceMgr->typeListener(Window::INTERFACE_TYPE, mMyWindowTypeListener.ptr()); interfaceMgr->typeNameType(Window::INTERFACE_TYPE_NAME, Window::INTERFACE_TYPE); // You need to store the mMyWindowTypeListener somewhere in your // module. If you delete it, your type listener will get destroyed. // The memory allocated for the listener is @e your responsibility. // You must make sure it gets uninstalled from the manager // (by interfaceMgr->typeListener(Window::INTERFACE_TYPE, 0) ) and // is freed when your module unloads. Using Ptr does the free'ing part // for you -- you just need to remember to uninstall your listener before // deleting it as usual.
The following is the easy way:
// When your module loads: mWindowTypeListener = interfaceMgr->newNamedInterfaceType<WindowImpl>(); // Likewise, store the returned Ptr<TypeListener> and keep it until // your module is unloaded. This code just creates and installs a listener // for you; so you'll need to uninstall the listener as above when you // exit.
This template method automatically pulls the WindowImpl::INTERFACE_TYPE and WindowImpl::INTERFACE_TYPE_NAME constants from your class (which are just the Window::INTERFACE_TYPE and Window::INTERFACE_TYPE_NAME constants your implementation class inherited) and then registers a default TypeListener which will generate instances of your type. In order for this to work, you must provide a constructor for the default listener to call. It should take parameters: (const Ptr<Description>& description, const String& name, NamedInterface::Identifier identifier, const NamedInterface::Manager* manager). The first parameter will be the constructorData that is normally passed to the listener, and the last parameter is the interface manager in which the listener is being installed. The name and identifier parameters are the name and identifier which will assigned to your entity after it is constructed.
For WindowImpl, we would provide:
WindowImpl::WindowImpl(const Ptr<Description>& unused, const String& name, NamedInterface::Identifier ident, NamedInterface::Manager* interfaceMgr) { }
You'll notice that we've blindly chosen to put the INTERFACE_TYPE constants in the Window interface definition. This has an important implication: when NamedInterface::Manager::newNamedInterface() is called to construct a new "Window", the client can only request an instance of the Window interface -- it cannot request a particular implementation. If a client could depend on a specific implementation, that would prevent us from using a new implementation without introducing incompatibility; which is the reason for separating interface and implementation in the first place. Though we haven't described how the Description works, understand that the "constructorData" parameter might have information in it which allows your TypeListener to select a particular implementation, but you should be careful not to abuse this and expose too much implementation to the client (e.g., don't put an "implementation()" field in your Description that allows the client to select an implementation for itself).
So now we have our Window entity, all set to go. You can instantiate an instance of the window like this:
// In your code Ptr<Window> myWindow = interfaceMgr->newNamedInterface(NULL, Window::INTERFACE_TYPE, "My Window"); // Elsewhere in the system, another client can gain access to // the window through the entity manager. Ptr<Window> yourWindow = interfaceMgr->namedInterface("My Window").dynamicCast<Window>(); Ptr<Window::PaintInterface> myPaintInterface = yourWindow->newPaintInterface();
Note the necessity for a dynamic cast of the pointer returned by namedInterface() (which has type Ptr<NamedInterface>). This is fine for as long as we have a single type of Window. In the case that we have multiple types of Window that the client might be interested in, we'll need to extend this a little bit so that we can have more than one type of Window entity.
// in PhxWindow.h ; or perhaps PhxWindowTypes.h class StandardWindow : public Window { public: const static NamedInterface::Type INTERFACE_TYPE; const static String INTERFACE_TYPE_NAME; }; class FloatingWindow : public Window { public: const static NamedInterface::Type INTERFACE_TYPE; const static String INTERFACE_TYPE_NAME; };
Now, we implement each of these interfaces in PhxWindow(Types).cpp:
class StandardWindowImpl : public StandardWindow { ... } class FloatingWindowImpl : public FloatingWindow { ... }
Also, since Window is no longer implemented (we have implementations for StandardWindow and FloatingWindow, but not for Window itself), we should leave out the INTERFACE_TYPE constant from the Window interface definition. Now we have two NamedInterface types (derived from Window, which is derived from NamedInterface). You can register each of them with the NamedInterface::Manager just as we did with Window before, only now we have two separate types and the client can request which they would like to use. An important property of providing new interface classes is that the client can explicitly distinguish between different Window types using typical casts:
Ptr<NamedInterface> myNamedInterface =
entityMgr->newNamedInterface(NULL, StandardWindow::INTERFACE_TYPE, "");
Ptr<Window> myWindow = myNamedInterface.dynamicCast<Window>();
// myWindow == NULL if myNamedInterface is not actually a Window.
Ptr<StandardWindow> myStdWindow = myNamedInterface.dynamicCast<StandardWindow>();
// myStdWindow == NULL if myNamedInterface is not actually a StandardWindow.
This would not be possible if we created both types as private implementations of Window. The client could dynamic_cast to Window, but couldn't easily determine which window type was behind the interface, since it doesn't (indeed, shouldn't) have access to the private implementation class. The rule: if the difference in types must be visible to the client, define the types as separate interface classes (which, presumably, are each a separate NamedInterface type).
A final example that hits closer to home would be the creation of dozens of Aircraft types. Clearly we will have a generic Aircraft interface that defines the common attributes that all aircraft share. Something like:
class Aircraft : public NamedInterface { public: // Note: No INTERFACE_TYPE here, since we probably can't create // "just an Aircraft" directly -- we can only create specific types. // attributes common to all aircraft virtual Force maxGrossWeight(void) const = 0; }; class F14 : public Aircraft { public: static const NamedInterface::Type INTERFACE_TYPE; static const String INTERFACE_TYPE_NAME; }; class F15 : public Aircraft { public: static const NamedInterface::Type INTERFACE_TYPE; static const String INTERFACE_TYPE_NAME; }; class F16 : public Aircraft { public: static const NamedInterface::Type INTERFACE_TYPE; static const String INTERFACE_TYPE_NAME; };
Of course, in reality our class hierarchy could have much more flavor, perhaps dividing up aircraft further by common characteristics (e.g., FighterAircraft or TransportAircraft). Either way, you can see that the visibly different types of aircraft are defined as more specific interfaces derived from more generic interfaces. The implementation of an F15 might share some implementation with other aircraft, but it could just as well reimplement the entire Aircraft interface itself. Just remember: the publicly exposed interface, as far as everyone else is concerned, is all that there is; so if you have multiple kinds of objects that you want to represent for the client, you'll want to make multiple interface types to do it.
Keep in mind that the interfaces in the header may be organized totally differently than in the implementation. It may be the case that we provide an F14 implementation and F15 separately, or maybe just use the same one for both interfaces. There could be complex interactions between the two types (perhaps their implementations share a pool of memory for some purpose or other). The client can't see this -- they only see what's in the interfaces in the header. If the client should see it, put it in the interface (or make it a separate interface); otherwise, hide it! Be stingy -- hide all you can.
class CommonAircraftImpl : public Aircraft { // common code for many (perhaps not all) Aircraft types void commonAttribute(double commonAttr) { mCommonAttr = commonAttr; } }; class F15Impl : public CommonAircraftImpl { }; // Ack! F15Impl must be derived from F15. class F15Impl2 : public F15 { F15Impl2() { mCommonAircraft = new CommonAircraftImpl(); } void commonAttribute(double commonAttr) { mCommonAircraft->commonAttribute(commonAttr); } }; // Now we're inheriting correctly.
The last part in the above snippet is one way to solve the problem. For small classes, this is a very simple solution and it's very easy to understand. For big classes, it's very tedious.
Thankfully, C++ templates offer another way to do this automatically:
template <class Interface> class CommonAircraftImpl : public Interface { void commonAttribute(double commonAttr) { mCommonAttr = commonAttr; } }; class F15Impl : public CommonAircraftImpl<F15> { };
All this does is allow us to define our CommonAircraftImpl and have it derive from the correct base class in each situation. We can layer these common code classes. For instance, if jet aircraft have some common code that is not common to all aircraft, then we could define a class like this:
template <class Interface> class JetAircraftImpl : public CommonAircraftImpl<Interface> { // code for aircraft with jet engines };
class F15Impl : public JetAircraftImpl<F15> { };
This will give us the common aircraft code, plus any additional or overridden functionality provided by the JetAircraftImpl class.
The one thing you're likely to run into with this is a common sickness of template code: templates beget templates. Once you templatize one class, you may find yourself templatizing many many classes in order for things to work correctly. Getting around this takes a little bit of thought and trickery. The best thing to do is just to try this method out when faced with the problem and learn from the process.
1.4.2