4.3.2 Implementation inheritance

Implementation inheritance is traditionally considered one of the three pillars of object-oriented programming, along with encapsulation and polymorphism. This supposed third pillar has been omitted from this object model.

Implementing support for single implementation inheritance would be straight-forward, and Stroustrup (1999) demonstrates that supporting multiple implementation inheritance adds very little in terms of complexity, runtime cost and memory overhead. Making implementation inheritance part of the binary standard of this object model, however, allowing classes written in different languages to extend one another, would add considerable complexity.

There are reasons to forego implementation inheritance completely. Snyder noted that implementation inheritance breaks encapsulation as early as 1986. Szyperski et al. (2002:115) offer a critique that centers on the tight dependency between classes and their ancestor classes, called the fragile base class problem. The syntactic variant of the problem refers to the inability to modify a class without recompiling all descendant classes and dependent clients, and is solely concerned with binary compatibility (as compiled-in offsets are no longer correct when the base class is modified). This problem was solved by IBM’s System Object Model (SOM), by initializing dispatch tables at load-time.

The semantic variant of the problem is more interesting, and is concerned with changes to the behavior of ancestor classes that a descendant class cannot cope with. If a class overrides selected (virtual) operations of an ancestor class, it may become dependent on the ancestor class calling these operations, perhaps even in a certain order. The dependence of the descendant class on the behavior of the ancestor class is not regulated by the formal (syntactic) contract between the two classes. The tight coupling that results makes evolution of the ancestor classes difficult or impossible.

The recommended way to avoid implementation inheritance is to use forwarding, sometimes known as delegation, which entails forwarding calls to internally-held objects.1 Forwarding calls to an internally held reference works just as well for code reuse as inheriting an implementation (with the downside that most languages require significantly more boiler-plate code to be written). It does not allow for the same level of customization of another class as that afforded by overriding operations of an ancestor class, though. Support for customization must be built into the ancestor class, and not patched in, as is done when virtual operations are overridden.

Footnotes

  1. Embarcadero’s Delphi programming language includes a language feature that makes forwarding less verbose than having to manually forward every call. This feature is described in the footnote in section 5.3.