[ Team LiB ] Previous Section Next Section

Gotcha #95: Overuse of Inheritance

Wide or deep hierarchies may indicate poor design. Often, such hierarchies occur due to inappropriate factoring of the hierarchy's responsibilities. Consider a simple hierarchy of shapes, as in Figure 9-5.

Figure 9-5. A shape hierarchy

graphics/09fig05.gif

As it turns out, these shapes are rendered in blue when drawn. Suppose a newly minted C++ programmer, fresh from a first exposure to inheritance, is given the task of extending this hierarchy of shapes to allow red shapes as well as blue. No problem.

As Figure 9-6 shows, we have a classic occurrence of an "exponentially" expanding hierarchy. To add a new color, we must augment the hierarchy with a new class for every shape. To add a new shape, we must augment the hierarchy with a new class for every color. This is silly, and the proper design is obvious. We should use composition instead, as in Figure 9-7.

Figure 9-6. An incorrect, exponentially expanding hierarchy

graphics/09fig06.gif

Figure 9-7. A correct design that employs inheritance and composition properly

graphics/09fig07.gif

A Square is-a Shape and a Shape has-a Color. Not all examples of overuse of inheritance are so obviously wrong. Consider a financial option hierarchy used to represent options on various types of financial instruments, as in Figure 9-8.

Figure 9-8. A poorly designed, monolithic hierarchy

graphics/09fig08.gif

Here we employ a single option base class, and each concrete option type is a combination of the type of option and the type of financial instrument to which the option is applied. Once again we have an expanding hierarchy, in which the addition of a single new option type or financial instrument will result in many classes being added to the option hierarchy. Typically, the proper design involves composition of simple hierarchies, as in Figure 9-9, rather than a single, monolithic hierarchy.

Figure 9-9. A correct design; composition of simple hierarchies

graphics/09fig09.gif

An Option has-a Instrument. These hierarchy difficulties are the result of poor domain analysis, but it's also common to produce an unwieldy hierarchy in spite of impeccable domain analysis. Continuing with our financial instruments, let's look at a simplified bond implementation:



class Bond { 


 public:


   // . . .


   Money pv() const; // calculate present value


};


The pv member function calculates the present value of a Bond. However, there may be several algorithms for performing the computation. One way to handle this would be to merge all the possible algorithms into a single function and select among them with a code:



class Bond { 


 public:


   // . . .


   Money pv() const;


   enum Model { Official, My, Their };


   void setModel( Model );


 private:


   // . . .


   Model model_;


};


Money Bond::pv() const {


   Money result;


   switch( model_ ) {


   case Official:


       // . . .


       return result;


   case My:


       // . . .


       return result;


   case Their:


       // . . .


       return result;


   }


}


However, this approach makes it hard to add new pricing models, since source change and recompilation are required. Standard object-oriented design practices tell us to employ inheritance and dynamic binding to implement variation in behavior, as in Figure 9-10.

Figure 9-10. Incorrect application of inheritance; use of inheritance to vary behavior of a single member function

graphics/09fig10.gif

Unfortunately, this approach fixes the behavior of the pv function when the Bond object is created, and it can't be changed later. Additionally, other aspects of a Bond's implementation may vary independently of the implementation of its pv function. This can lead to a combinatorial explosion in the number of derived classes.

For example, a Bond may have a member function to calculate the volatility of its price. If this algorithm is independent of that for calculating its present value, an additional pricing algorithm or volatility algorithm will require that new derived classes in every new combination of price/volatility calculation be added to the hierarchy. Generally, we use inheritance to implement variation of the entire behavior of an object, not just of a single operation.

As with our earlier example with colored shapes, the correct solution is to employ composition. In particular, we'll employ the Strategy pattern to reduce our monolithic Bond hierarchy to a composition of simple hierarchies, as in Figure 9-11.

Figure 9-11. Correct use of Strategy to express independent variation of behavior of two member functions

graphics/09fig11.gif

The Strategy pattern moves the implementation of an algorithm from the body of the function to a separate implementation hierarchy:



class PVModel { // Strategy 


 public:


   virtual ~PVModel();


   virtual Money pv( const Bond * ) = 0;


};


class VModel { // Strategy


 public:


   virtual ~VModel();


   virtual double volatility( const Bond * ) = 0;


};


class Bond {


   // . . .


   Money pv() const


       { return pvmodel_->pv( this ); }


   double volatility() const


       { return vmodel_->volatility( this ); }


   void adoptPVModel( PVModel *m )


       { delete pvmodel_; pvmodel_ = m; }


   void adoptVModel( VModel *m )


       { delete vmodel_; vmodel_ = m; }


 private:


   // . . .


   PVModel *pvmodel_;


   VModel *vmodel_;


};


Use of Strategy allows us to both simplify the structure of the Bond hierarchy and change the behavior of the pv and volatility functions easily at runtime.

    [ Team LiB ] Previous Section Next Section