|< Free Open Study >|
5.13 The Confusion of the Need for Inheritance Versus an Object's Dynamic Semantics
Let us examine a new inheritance hierarchy that was discovered through application of Heuristic 5.13. While examining a Stack class, a designer realized that the pop method was performing case analysis on the value of the Stack_pointer data member. If it was zero, the stack could not pop, and if it was greater than zero, it had an algorithm for popping. The design was further substantiated when he or she realized that empty stacks know how to push but nonempty stacks know how to both push and pop (see Figure 5.29). That is, the pop method is really an added functionality.
What do you think of this design? Everything seems satisfactory until we think about the life of a stack object. It is created as an empty stack object; someone executes a push operation and the empty stack object is converted to a nonempty stack object. Later, someone executes a pop operation and the nonempty stack object turns back into an empty stack object. The object keeps toggling its type at runtime. In most object-oriented implementations, changing the type of an object at runtime is an expensive operation. It requires constructing an object of the new class using a constructor for the new class which accepts an object of the old class as an argument. The old object must then be destroyed upon return from the constructor.
What has caused this problem? It turns out that explicit case analysis on the value of an attribute is sometimes the implementation of the dynamic semantics of an object (i.e., its states and their transitions). If a designer attempts to capture these dynamic semantics using the static semantics of inheritance, a toggling of types is the result. Whenever an object would have changed its state in the old design, an object of one derived class must be changed to an object of another derived class in the new design. This is highly inefficient and confusing. The modeling of an object's legal states as classes is another cause of proliferation of classes, albeit a trivial one. The designer usually detects this toggling problem at design time, and certainly at implementation time. While the stack class in Figure 5.29 does not look like a serious proliferation problem, consider the state-transition diagram in Figure 2.9 for the class Process (of an operating system). If each state of the Process class is modeled as its own distinct class, then we would be adding five new classes to our system. In addition, many objects would be toggling their types at runtime. In any event, users of a class should not be aware of the mechanics of its states and/or their transitions. These items are implementation details of the class.
How do we implement dynamic semantics? The preferred method is to perform explicit case analysis on the values of the attributes that capture the state information. While explicit case analysis is undesirable, at least this case analysis is used only by implementors of the class. Explicit case analysis on the types of objects is often performed by the users of a class, a much nastier maintenance problem. Also, it is much more likely for applications to get a new type than for a class to get a new state. James Coplien's letter/envelope idiom can be used to provide an interesting polymorphic solution to this problem . I call this solution a dynamic semantic wrapper and display it in Figure 5.30. The idea is to capture the states of a class in an inheritance hierarchy internal to the class. A state field of the class will toggle types, but since the state class does not contain data, type changes are extremely cheap (one-integer assignment in the case of C++).
The problem with this solution is that inheritance hierarchies tend to get a bit unruly in large applications. Having each class with interesting dynamic semantics contain its own inheritance hierarchy adds complexity to the design. Many developers do not feel this added complexity is a good trade-off considering they only get out of explicit case analysis in the hidden implementation of the class. However, as Coplien correctly points out, some classes have very complex state behavior that certainly benefits from the distributive effects of the inheritance solution.
|< Free Open Study >|