[ Team LiB ] |
![]() ![]() |
Gotcha #97: Cosmic HierarchiesMore than a decade ago, the C++ community decided that the use of "cosmic" hierarchies (architectures in which every object type is derived from a root class, usually called Object) was not an effective design approach in C++. There were a number of reasons for rejecting this approach, both on the design level and on the implementation level. From a design standpoint, cosmic hierarchies often give rise to generic containers of "objects." The content of these containers are often unpredictable and lead to unexpected runtime behavior. Bjarne Stroustrup's classic counterexample considered the possibility of putting a battleship in a pencil cup—something a cosmic hierarchy would allow but that would probably surprise a user of the pencil cup. A pervasive and dangerous assumption among inexperienced designers is that an architecture should be as flexible as possible. Error. Rather, an architecture should be as close to the problem domain as possible while retaining sufficient flexibility to permit reasonable future extension. When "software entropy" sets in and new requirements are difficult to add within the existing structure, the code should be refactored into a new design. Attempts to create maximally flexible architectures a priori are similar to attempts to create maximally efficient code without profiling; there will be no useful architecture, and there will be a loss of efficiency. (See also Gotcha #72.) This misapprehension of the goal of an architecture, coupled with an unwillingness to do the hard work of abstracting a complex problem domain, often results in the reintroduction of a particularly noxious form of cosmic hierarchy: class Object { public: Object( void *, const type_info & ); virtual ~Object(); const type_info &type(); void *object(); // . . . }; Here, the designer has abdicated all responsibility for understanding and properly abstracting the problem domain and has instead created a wrapper that can be used to effectively "cosmicize" otherwise unrelated types. An object of any type can be wrapped in an Object, and we can create containers of Objects into which we can put anything at all (and frequently do). The designer may also provide the means to perform a type safe conversion of an Object wrapper to the object it wraps: template <class T> T *dynamicCast( Object *o ) { if( o && o->type() == typeid(T) ) return reinterpret_cast<T *>(o->object()); return 0; } At first glance, this approach may seem acceptable (if somewhat ungainly), but consider the problem of extracting and using the content of a container that can contain anything at all: void process( list<Object *> &cup ) { typedef list<Object *>::iterator I; for( I i(cup.begin()); i != cup.end(); ++i ) { if( Pencil *p = dynamicCast<Pencil>(*i) ) p->write(); else if( Battleship *b = dynamicCast<Battleship>(*i) ) b->anchorsAweigh(); else throw InTheTowel(); } } Any user of the cosmic hierarchy will be forced to engage in a silly and childish "guessing game," the object of which is to uncover type information that shouldn't have been lost in the first place. In other words, that a pencil cup can't contain a battleship doesn't indicate a design flaw in the pencil cup. The flaw may be found in the section of code that thinks it's reasonable to perform such an insertion. It's unlikely that the ability to put a battleship in a pencil cup corresponds to anything in the application domain, and this is not the type of coding we should encourage or submit to. A local requirement for a cosmic hierarchy generally indicates a design flaw elsewhere. Since our design abstractions of pencil cups and battleships are simplified models of the real world (whatever "real" means in the context), it's worth considering the analogous real-world situation. Imagine that, as the designer of a (physical) pencil cup, you received a complaint from one of your users that his ship didn't fit in the cup. Would you offer to fix the pencil cup, or would you offer some other type of assistance? The repercussions of this abdication of design responsibility are extensive and serious. Any use of a container of Objects is a potential source of an unbounded number of type-related errors. Any change to the set of object types that may be wrapped as Objects will require maintenance to an arbitrary amount of code, and that code may not be available for modification. Finally, because no effective architecture has been provided, every user of the container is faced with the problem of how to extract information about the anonymous objects. Each of these acts of design will result in different and incompatible ways of detecting and reporting errors. For example, one user of the container may feel just a bit silly asking questions like "Are you a pencil? No? A battleship? No? …" and opt for a capability-query approach. The results are not much better (see Gotcha #99). Often, the presence of an inappropriate cosmic hierarchy is not as obvious as it is in the case we just discussed. Consider a hierarchy of assets, as in Figure 9-13. Figure 9-13. An iffy hierarchy. It's not clear whether the use of Asset is overly general or not.It's not immediately clear whether the Asset hierarchy is overly general or not, especially in this high-level picture of the design. Often the suitability of a design choice is not clear until much lower-level design or coding has taken place. If the general nature of the hierarchy leads to certain disreputable coding practices (see Gotchas #98 and 99), it's probably a cosmic hierarchy and should be refactored out of existence. Otherwise, it may simply be an acceptably general hierarchy. Sometimes, refactoring our perceptions can improve a hierarchy, even without source code changes. Many of the problems associated with cosmic hierarchies have to do with employing an overly general base class. If we reconceptualize the base class as an interface class and communicate this reconceptualization to the users of the hierarchy, as in Figure 9-14, we can avoid many of the damaging coding practices mentioned earlier. Figure 9-14. An effective reconceptualization. The is-a relationship is appropriately weakened if we consider Asset to be a protocol rather than a base class.Our design no longer expresses a cosmic hierarchy but three separate hierarchies that leverage independent subsystems through their corresponding interfaces. This is a conceptual change only, but an important one. Now employees, vehicles, and contracts may be manipulated as assets by an asset subsystem, but the subsystem, because it's ignorant of classes derived from Asset, won't attempt to uncover more precise information about the Asset objects it manipulates. The same reasoning applies to the other interface classes, and the possibility of a runtime type-related error is small. |
[ Team LiB ] |
![]() ![]() |