[ Team LiB ] Previous Section Next Section

Gotcha #92: Public Inheritance for Code Reuse

Class hierarchies promote reuse in two ways. First, they permit code common to different derived class implementations to be placed in a shared base class. Second, they permit the base class interface to be shared by all publicly derived classes. Both code sharing and interface sharing are desirable goals in hierarchy design, but interface sharing is the more important of the two.

Use of public inheritance primarily for the purpose of reusing base class implementations in derived classes often results in unnatural, unmaintainable, and, ultimately, more inefficient designs. The reason is that a priori use of public inheritance for code reuse may constrain the base class interface to the extent that it may be difficult to design substitutable derived classes. This, in turn, may restrict the extent to which generic code written to the base class "contract" may be leveraged by derived classes. Typically, much more code reuse is achieved by leveraging large amounts of generic code than by sharing a modest amount in the base class.

The advantages of leveraging generic code written to a base class contract are so extensive that it often makes sense to facilitate this by designing a hierarchy with an interface class at its root. An "interface class" is a base class with no data, a virtual destructor, and typically all pure virtual member functions and no declared constructor. Interface classes are sometimes called "protocol classes," since they specify a protocol for using a hierarchy without any associated implementation. (A "mix-in" is similar to interface class, but a mix-in may contain some minimal data and implementation.)

Using an interface class at the root of a hierarchy eases later maintenance of the hierarchy by simplifying the application of patterns like Decorators, Composites, Proxies, and so on. (Using interface classes also mitigates technical problems associated with the use of virtual base classes; see Gotcha #53.)

The canonical example of an interface class is the use of the Command pattern to implement an abstract callback hierarchy. For instance, we may have a GUI Button class that executes a callback Action when pressed:

gotcha92/button.h



class Action { 


 public:


   virtual ~Action();


   virtual void operator ()() = 0;


   virtual Action *clone() const = 0;


};


class Button {


 public:


   Button( const char *label );


   ~Button();


   void press() const;


   void setAction( const Action * );


 private:


   string label_;


   Action *action_;


};


The Command pattern encapsulates an operation as an object so all the advantages of using an object may be leveraged for the operation. In particular, we'll see below that use of the Command pattern allows us to apply additional patterns to our design.

Note the use of an overloaded operator () in the implementation of Action. We could have used a non-operator member function named execute, but the use of an overloaded function call operator is a C++ coding idiom that indicates Action is an abstraction of a function, in the same way use of an overloaded operator -> in a class indicates that objects of the class are to be used as "smart pointers" (see Gotchas #24 and #83). The Action class also employs the Prototype pattern through the declaration of the clone member function, which is used to create a duplicate of an Action object without precise knowledge of its type (see Gotcha #76).

Our first concrete Action type employs the Null Object pattern to create an Action that does nothing in such a way that all the requirements of an Action are satisfied. A NullAction is-a Action:

gotcha92/button.h



class NullAction : public Action { 


 public:


   void operator ()()


       {}


   NullAction *clone() const


       { return new NullAction; }


};


With the Action framework in place, it's trivial to produce a safe and flexible Button implementation. Use of Null Object ensures that a Button will always do something if pressed, even if "doing something" means doing nothing (see Gotcha #96).

gotcha92/button.cpp



Button::Button( const char *label ) 


   : label_( label ), action_( new NullAction ) {}


void Button::press() const


   { (*action_)(); }


Use of Prototype allows a Button to have its own copy of an Action while remaining ignorant of the exact type of Action it copies:

gotcha92/button.cpp



void Button::setAction( const Action *action ) 


   { delete action_; action_ = action->clone(); }


This is the basis of our Button/Action framework, and, as in Figure 9-2, we can add concrete operations (that, unlike NullAction, actually do something) without the necessity of recompiling the framework.

Figure 9-2. Instances of the Command and Null Object patterns used for Button callback operations

graphics/09fig02.gif

The presence of an interface class at the root of the Action hierarchy allows us to additionally augment the hierarchy's capabilities. For example, we could apply the Composite pattern to allow a tree of Actions to be executed by a Button:

gotcha92/moreactions.h



class Macro : public Action { 


 public:


   void add( const Action *a )


       { a_.push_back( a->clone() ); }


   void operator ()() {


       for( I i(a_.begin()); i != a_.end(); ++i )


           (**i)();


   }


   Macro *clone() const {


       Macro *m = new Macro;


       for( CI i(a_.begin()); i != a_.end(); ++i )


           m->add((*i).operator ->());


       return m;


   }


 private:


   typedef list< Cptr<Action> > C;


   typedef C::iterator I;


   typedef C::const_iterator CI;


   C a_;


};


The presence of a lightweight interface class at the root of the Action hierarchy enabled us to apply the Null Object and Composite patterns, as shown in Figure 9-3. The presence of significant implementation in the Action base class would have forced all derived classes to inherit it and any side effects the initialization and destruction of the inherited implementation entailed. This would effectively prevent the application of Null Object, Composite, and other commonly used patterns.

Figure 9-3. Augmenting the Action hierarchy with an application of the Composite pattern

graphics/09fig03.gif

However, there is a tension between the flexibility of an interface class and the sharing and (often) marginally better performance that obtains with a more substantial base class. For example, it's possible that many of the concrete classes derived from Action have duplicate implementation that could be shared by placing the implementation in the Action base class. However, doing so would compromise our ability to add additional functionality to the hierarchy, as we did above with the application of the Composite pattern. In cases like this, it may be permissible to attempt to get the best of both worlds through the introduction of an artificial base class that is purely for implementation sharing, as in Figure 9-4.

Figure 9-4. Introduction of an artificial base class to allow both interface and implementation sharing

graphics/09fig04.gif

However, overuse of this approach may result in hierarchies with many artificial classes that have no counterpart in the problem domain and are, as a result, hard to understand and maintain.

In general, it's best to concentrate on inheritance of interface. Proper and efficient code reuse will follow automatically.

    [ Team LiB ] Previous Section Next Section