Inheritance


Inheritance myths

Various well-meaning authors have advocated the following design rules for inheritance: In fact, none of these proposed rules is good advice universally. The appropriate rootedness, depth, and fanout for an inheritance hierarchy depend on the domain the hierarchy is intended to model and on the desired properties of the hierarchy (extensibility, efficiency, etc). [Martin Carroll, Margaret Ellis, Designing and Coding Reusable C++, Addison-Wesley, 1995, p178]


Types of inheritance

"In general, inheritance is applicable only if you can seriously argue for the presence of an "is" relation between the instances of the heir and parent classes ... A widely circulated interpretation of this criteria holds that inheritance should be used for strictly limited purposes, and that any form of "implementation inheritance" or "inheritance for code reuse" is bad. I find this view too restrictive."

"My own view of inheritance is broad. My colleagues and I have defined and use the eleven categories described below extensively. I find them both theoretically legitimate and practically indispensible."

Model inheritance - "isa" relationships

Variation inheritance - how one class differs from another Software inheritance - implementation issues [Source: Bertrand Meyer, "The many faces of inheritance: a taxonomy of taxonomy", IEEE Computer, May 96, pp105-108]


Inheritance with addition/extension

Extension inheritance introduces new attributes or methods in derived classes. An apparent example might be classes Rectangle and Square. An early OO library had its Rectangle class inherit from the Square class: Square has a side attribute, Rectangle inherits from Square and adds a new attribute - other_side. "This was criticized and soon corrected." [Bertrand Meyer]

In OO software, it is not uncommon to extend a base class by having derived classes introduce new "features". A legitimate example might be a class MovingPoint that inherits off of class Point, and adds an attribute called Velocity. Meyer off-handedly comments, "in most applications, of course, Square should inherit Rectangle, not the reverse." But he goes on to maintain that "we cannot dismiss the general category of extension inheritance", because it offers real and practical benefit.

Proper use of inheritance to implement subtying according to the Liskov Substitution Principle can be achieved using inheritance with addition. Adding new member functions to a class intuitively reduces the number of objects it characterizes. For example, adding a speed calling function to a simple phone creates a new phone abstraction, with more functions than the original, and a smaller subset of applicable instances. Because existing operations are left intact, an object of the derived class can still be used where a base class object is expected. However, this also means that software manipulating such objects in terms of their base class interface will not be able to exercise the new member functions added at lower levels. [Coplien, p232]

Using inheritance with addition, we can add new member functions in derived classes but cannot access those functions in terms of their base class interface. This is quite a restrictive model of inheritance and leaves no room for run-time dispatching of the new functions (how the object paradigm derives much of its power). [Coplien]


Inheritance with restriction

In this mode of inheritance, the only conceptual change from a base class to a derived class is the addition of some contraint(s). Many mathematical examples fall into this category, including class Circle inheriting from class Ellipse. The extra constraint is that the two focuses of an ellipse are merged into one point for a circle. Any new "features" added should be limited to those that directly follow from the constraint(s). [Bertrand Meyer]

An example: class Circle has a new feature called "radius" which captures the distinguishing characteristic of instances of Circle (the common distance of all points from the merged center). This deserves the status of an attribute of class Circle, whereas the corresponding notion in class Ellipse (the average of the distances to the two focuses) is probably not significant enough to yield an attribute. [Bertrand Meyer]

The presence of both the restriction and extension variants of inheritance is one of its paradoxes. But Meyer maintains, "I find them both theoretically legitimate and practically indispensible."

If a circle is anything, it is a specialized form of ellipse with the two foci at the same point (the same way a square is a specialized form of reactangle with all four sides equal). When you use the word "specialized" or "particular", as in "a particular kind of" to describe a relationship, you may have a good candidate for inheritance. [Ken Arnold, "Class design for inheritance", Unix Review, Jul 94, p68]


Inheritance with cancellation

Some object-oriented languages allow base class properties to be "subtracted" or "cancelled" in a derived class. C++ disallows cancellation in public inheritance. This can be illustrated with the following code fragment.
   class Base {
   public:
      void function1();
      void function2();
   };

   class Derived : public Base {
   public:
      void function3();
   private:
      Base::function1;   // cancellation (not valid C++)
   };
However, if private derivation is used (effectively cancelling all the public operations of the base class), the base class operations can be made accessible again one by one.
   class List {
   public:
      void insert(...);
      int  count();
      int  has(...);
   };

   class Set : private List {
   public:
      void insert(...);   // redefining insert()
      List::count;        // making count() accessible
      List::has;          // making has() accessible
   };
"Derivation with cancellation may be a hint that an inappropriate inheritance structure has been put in place. In general, problems abound in the area of cancellation, and you should avoid it as a design technique, and even as a design accident, with great care." [Coplien, p239]

It is possible to simulate inheritance with cancellation in C++ using private inheritance. This should normally be avoided, as it increases complexity in the inheritance structure. In any case, it is likely that it is really another type of relationship (has-a) that has been wrongly implemented with inheritance(is-a). [Karlsson, p329]


Private inheritance

"When using inheritance for convenience or reuse, use private inheritance. Public inheritance reflects subtyping relationships, not reuse." [Coplien, p277]

"A derived class can override virtual member functions called by functions in a private base class, so private inheritance works better than containment for two classes collaborating for reuse. A base class defines overall structure and characterizes obligations on the derived class as pure virtual functions. The base class can depend on an implementation of these pure virtuals in the derived class, as the derived class can invoke (and reuse) the base class member functions. [Coplien, p268]

"A client might want to inherit a class's implementation but not its interface. Private derivation accomplishes this kind of inheritance. A client might want to inherit both a class's interface and its implementation. Public derivation accomplishes this kind of inheritance." [Martin Carroll, Margaret Ellis, Designing and Coding Reusable C++, Addison-Wesley, 1995, p178]

Use private inheritance judiciously. A common usage is to combine public inheritance of an interface with private inheritance of an implementation. [Meyers, 50 Ways, pp145-150,164]

Private inheritance models "implemented in terms of". Since this can be accomplished by simple containment ("has a"), I see no reason to use this kind of inheritance and eschew it religiously. It muddles your type with your implementation to save you a few keystrokes. Minor changes to your keystroke count are much less important than the clarity of your class to your users and to others who maintain it. [Ken Arnold, p67]

Favor object composition over class inheritance. [GOF, p20]

"Do not use private inheritance. It is not an object-oriented concept and it is not used in any object-oriented design methodology." [Karlsson, p331]


Multiple inheritance

"Multiple inheritance is a powerful feature, rich in semantics and able to directly express complex structures. It should be used sparingly, when single inheritance (or no inheritance at all) would lose important design information, not encapsulate change in the long term, or compromise the suitability of the solution to the problem." [Coplien, p179]

Use multiple inheritance rarely. It may cause difficulties when reusing components. Most object-oriented languages have problems when managing multiple inheritance, especially if the inheritance structure is ambiguous. Multiple inheritance is not common in the real world and it is important to focus on modelling the real world. [Karlsson, p312]

Use multiple inheritance judiciously. The one indisputable fact about multiple inheritance in C++ is that it opens up a Pandora's box of complexities that simply do not exist under single inheritance. Of these, the most basic is ambiguity. Suppose: a Lottery class has the method draw(), a GraphicalObject class has the method draw(), and a LotterySimulation class wants to inherit from both. Everything is fine until the client of LotterySimulation calls the draw() method - the compiler will not accept this ambiguity. [The solution is to provide a "fully qualified path" to the draw() that is desired.] Another sore spot is the "deadly diamond inheritance hierarchy". Using virtual base classes mostly remedy the problems, but they introduce their own suite of complexities. Limiting the bulk of your base classes to the pure abstract base class form routinely makes things much more manageable. [Meyers, 50 Ways, pp157-169]

Classes in the Taligent environment are artificially partitioned into two categories: domain classes that represent fundamental abstractions (like a car), and mixin classes that represent optional functionality (like power steering). Taligent suggests the rule:

A class may inherit from zero or one domain classes, plus zero or more mixin classes.
The effect of this guideline is that domain classes form a conventional, tree-structured inheritance hierarchy rather than an arbitrary acyclic graph. This makes the domain class hierarchy much easier to understand. Mixins then become add-in options that do not fundamentally alter the inheritance hierarchy. [Taligent, p17]

You can, and should, use multiple inheritance in other ways as well if it makes sense. [Taligent]


Obstacles to inheritability

The principal mechanism a C++ library uses to provide extensibility is inheritance. [Note: some authors use the term extensibility to refer to any kind of flexibility - including features like templates.] It can be difficult to derive from classes not written carefully to allow inheritance. Obstacles to inheritability include: "Because most of the obstacles to inheritability cannot occur in interface classes (see next section), libraries for which extensibility is important should interface all the classes whose interfaces users might wish to inherit". [Carroll and Ellis, pp54-70]


Inheritance versus Composition

Reuse by subclassing is often referred to as "white-box" reuse. Object composition is an alternative to class inheritance. This style of reuse is called "black-box" reuse, because no internal details of objects are visible. The problems with inheritance include: The opportunities with composition are: Our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition.

Favor object composition over class inheritance. [GOF, pp19-20]

Programmers modify the behavior of white-box frameworks by applying inheritance to override methods in subclasses of framework classes. Because it is necessary to override methods, programmers have to understand the framework's design and implementation, at least to a certain degree of detail. Black-box frameworks offer ready-made components for adaptations. Modifications are done by composition, not by inheritance. Frameworks will evolve more and more into black-box frameworks as they mature. [Wolfgang Pree, "Essential framework design patterns", Object Magazine, Mar 97, p34]

Future frameworks will rely mainly on composition. Frameworks that rely on inheritance are susceptible to the "fragile base class problem" - changes to a base class can fracture all the classes inheriting from it. This can be alleviated by designing frameworks in a way that allows most adaptations by composition instead of inheritance. [Wolfgang Pree, "Frameworks - past, present, future", Object Magazine, May 96, p25]

Dynamic, run-time simulation of inheritance can be done with something like delegation. Delegation is to object instances what inheritance is to classes: the ability to share code and automatically use one abstraction's code to field requests made of another. [Coplien, p160]

Delegation is a way of making composition as powerful for reuse as inheritance. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate. This is analogous to subclasses deferring requests to parent classes. Delegation is an extreme example of object composition. It shows that you can always replace inheritance with object composition as a mechanism for code reuse. [GOF, p20]


Inheritance versus Templates

"Templates are an alternative to broad, shallow inheritance hierarchies constructed for code reuse." [Coplien, p257]

"Templates and inheritance are both powerful features of C++. Unfortunately, there is some confusion among C++ programmers about whether to base a design for a class hierarchy on templates or on inheritance. Library designers sometimes use inheritance when use of templates would produce a better design." The drawbacks of inheritance-based container classes discussed here include:

[Source: Carroll and Ellis, p192]

The question you must ask yourself is this: does the type I am trying to model in my domain actually affect the behavior of the class in my design? If the type does not affect the behavior, you can use a template. If the type does affect the behavior, you'll need virtual functions, and you'll therefore use inheritance ... The hallmark of a template class: the behavior doesn't depend on the type at all. [Meyers, 50 Ways, pp150-157]

Here's a rule of thumb to try for choosing between inheritance and templates -

Inheritance models relationships in the problem domain. Templates are about implementation details. Parameterized types and functions allow the reuse of chunks of implementation and are essentially ways of pulling common code out of several different classes and sharing that code among them. Inheritance is about the relationships and common behaviors between the domain entities we are trying to model.

For the inheritance relationship to be worthwhile, we need to have reasons to treat objects of the two classes interchangeably.

If the two classes are instantiations of a single template, however, no such relationship holds between them. The types are not related, they have no common ancestor, and code that manipulates objects of one type will not expect to manipulate objects of the other type. Indeed, the types of the classes are not related at all; only the implementations of those types are shared.

Think of templates as automated cut-and-paste, or macros on steriods. They allow sharing of code, but only in the sense of sharing sequences of characters that go through the compiler.

Sharing code on the inside of a class should not be confused with inheritance, which makes a much stronger claim. When two classes are related by inheritance, they share far more than the accidental features of the code that is used to implement them. Such classes are bound so closely that at some point they are indistinguishable to the code that manipulates instances of the classes. If that is the relationship you want to reflect, get your code reuse by inheritance. If you just want to make sure you don't have to copy all the changes, however, use templates.

[Source: Jim Waldo, "Choose your reuse", Unix Review, Dec 93, pp73-76]