Composition by inheritance

Instructor's Guide


intro, components, case study, crossing boundaries, styles, platform, summary, Q/A, literature
Inheritance is the composition mechanism par excellence for object-oriented programming. As argued before, inheritance allows for virtually unlimited factorization and hence reuse of code. Inheritance, however, is increasingly recognized as a mechanism that is difficult to control. Moreover, the programmer employing inheritance, when working with a statically typed language like Eiffel or C++, is faced with the problem of fitting the inheritance structure within the type system by designing an adequate type structure. In this section, we will look at the mechanisms available to develop hierarchic type structures and ways to check that derived classes meet the criterion of behavioral conformance as expressed in section contracts. Brief examples will be presented to illustrate the problems and pitfalls of which the programmer must be aware. In particular, we will discuss the mechanism of explicit scoping, the difficulties involved in satisfying the invariant properties for derived classes, and the use of multiple inheritance with shared base classes (which results in structures having a diamond property).

Virtual functions and scoping

Scoping, when used in combination with recursive virtual functions, may lead to unexpected problems. In slide 7-recursion, an example is given illustrating a problem that may occur when defining a derived class that relies for its functionality on the original methods defined for the base class.

Virtual functions and recursion

  class P { 
P
public: virtual int m(int i) { cout << i; return i==0 ? i : m(i-1); } }; class C : public P {
C
public: virtual int m(int i) { cout << "start"; return i<0 ? 0 : P::m(i); } };

slide: Virtual functions -- recursions

In the example, the derived class C is used to provide a better interface to the functionality P offers, by including an additional test in C::m. This test prevents P::m from diverging. However, the statement
   P* p = new C(); p->m(k); 
C::m

slide: Example

for any k > 0 will not result in printing the sequence 0..k backwards, since (inadvertently) the recursive call to m in P::m invokes C::m instead of P::m.

Solution -- explicit scoping

  class P { 
P'
public: virtual int m(int i) { cout << i; return i==0 ? i : P::m(i-1); } };

slide: Explicit scoping

The solution, simple as it is, is to apply explicit scoping in the recursive call to m, as shown in slide 7-explicit. Beware, recursion is often indirect!

Inheritance and invariance

When developing complex systems or class libraries, reliability is of critical importance. As shown in section ASSERT, assertions provide a means by which to check the runtime consistency of objects. In particular, assertions may be used to check that the requirements for behavioral conformance of derived classes are met.

Invariant properties -- algebraic laws

  class employee {
  public:
  employee( int n = 0 ) : sal(n) { }
  employee* salary(int n) { sal = n; return this; }
  virtual long salary() { return sal; }
  protected:
  int sal;
  };
  

Invariant

     k == (e->salary(k))->salary() 
  

slide: Invariant properties as algebraic laws

Invariant properties, however, are often conveniently expressed in the form of algebraic laws that must hold for an object. See section validation. Naturally, when extending a class by inheritance (to define a specialization or refinement) the invariants pertaining to the base class should not be disrupted. Although we cannot give a general guideline to prevent disruption, the example in slide 7-laws clearly suggests that hidden features should be carefully checked with respect to the invariance properties of the (derived) class. The example is taken from  [Bar92]. Below, we have defined a class employee. The main features of an employee are the (protected) attribute sal (storing the salary of an employee) and the methods to access and modify the salary attribute. For employee objects, the invariant (expressing that any amount k is equal to the salary of an employee whose salary has been set to k) clearly holds. Now imagine that we distinguish between ordinary employees and managers by adding a permanent bonus when paying the salary of a manager, as shown in slide 7-hidden-bonus. The reader may judge whether this example is realistic or not.

Problem -- hidden bonus

  class manager : public employee {
  public:
  long salary() { return sal + 1000; }
  };
  

Invariant

      k =?= (m->salary(k))->salary() 
  

slide: Violating the invariant

Then, perhaps somewhat to our surprise, we find that the invariant stated for employees no longer holds for managers. From the perspective of predictable object behavior this is definitely undesirable, since invariants are the cornerstone of reliable software (so to speak). The solution to this anomaly is to make the assignment of a bonus explicit, as shown in slide 7-explicit-bonus.

Solution -- explicit bonus

  class manager : public employee {
  public:
  manager* bonus(int n) { sal += n; return this; }
  };
  

Invariant -- restored

       k + n == ((m->salary(k))->bonus(n))->salary() 
  

slide: Restoring the invariant

Now, the invariant pertaining to managers may be strengthened by including the effects of assigning a bonus. As a consequence, the difference in salary no longer occurs as if by magic but is directly visible in the interaction with a manager object, as it should be.

Multiple inheritance {\em -- the diamond structure}

Object-oriented programming languages provide the technology for a component-based approach to software development. Inheritance may be regarded as a means to extend components and to refine the functionality of a given component. Usually, a component is taken to be identical to an object. However, the notion of a component may be extended to include multiple objects, each representing a sub-component responsible for some aspect of the component (such as, for example, its functional behavior or the display of its internal state in a window). Below, an (abstract) example will be given of how multiple inheritance may be used to organize the objects comprising the realization of a component in a flexible way. We employ in addition an abstract interface class, which is shared by the objects corresponding to the sub-components of a structure. We call the resulting structure a diamond structure for obvious reasons. An abstract interface class is intended to provide an abstract interface to clients of the component. This interface will usually be the most stable part of the component. The actual realization is more likely to change.

An abstract interface

  class A { 
A
public: virtual void operator()() = 0; virtual void value() = 0; virtual void display() = 0; };

slide: An abstract interface

The class A provides an apply operator (that is operator()), a method value() (that delivers an integer value) and a method display() (that displays the value). See slide 7-diamond-A. Note that this is just an example intended to illustrate the architecture of a diamond structure. In a more realistic example, the interface would no doubt offer numerous other services. Next, to define a partial realization of the interface class A, we define a model class M. Since A is intended to be shared by other sub-components as well, we employ virtual inheritance here. See slide 7-diamond-M.

Model -- functional behavior

  class M : virtual public A { 
\fbox{M}
public: M( int k ) : n(k) { } void operator()() { n++; } int value() { return n; } protected: int n; };

slide: The Model class

The class M actually defines the apply operator and the method value. The model class M represents the sub-component embodying the functional behavior of the AMVR diamond structure. Note that the definition of M may change without affecting the interface provided by A, in other words without disturbing the clients of the component. As the second sub-component providing a partial realization of the abstract class A, we need a class to display the value of the component (as realized by M). To this end we introduce the class V, as defined in slide 7-diamond-V.

View -- for display

  class V : virtual public A {  
\fbox{V}
public: V( int h, int w ) : d(new Display(h,w)) { } void display() { d->put( value() ); } private: Display* d; };

slide: The View class

A V object creates a new display, with a certain width and height, and puts the value on display when requested to do so. The display sub-component class V is likely to be the most volatile part of the structure. As observed in  [Guimaraes91], who introduced the technique discussed here, separating the window environment dependent aspects into a class allows for a relatively easy port to a variety of such environments. We employ multiple inheritance to combine the functional sub-component and the display sub-component. In other words, the class R is a realization of the abstract interface class A, combining the partial realizations M and V. See slide 7-diamond-R.

Realization -- used for creation

  class R : public M, public V { 
R
public: R( int k=0, int h=10, int w=20 ) : M(k), V(h,w) {} };

Usage

     A* a = new R(); (*a)(); a->display(); 
  

slide: The Realization class

Due to the use of virtual inheritance, instances of R have only one copy of A. If virtual inheritance had not been used then instances of R would contain two copies of A, one coming from M and one from V. Note that, whereas clients only need to know about the abstract interface class A, when an actual object realizing A must be created the realization type R must be explicitly used. Below, we will discuss a technique that allows us to hide the representation type during creation as well.

Hiding realizations

  class A { 
A
public: A() { body = new R(); } virtual void operator()() { body->operator()(); } virtual void value() { return body->value(); } virtual void display() { return body->display(); } protected: R* body; };

slide: Hiding realizations

Hiding realizations

Diamond structures provide a flexible means to develop portable and extensible code. However, a drawback of the approach described above is that the programmer needs to be aware of the structure of the realization of the component. Using the handler/body idiom introduced in section canonical this may easily be avoided, as shown in slide 7-diamond-hiding. When creating an instance of A, an instance of R is (implicitly) created which is attached to the body pointer. All calls to instances of A will be delegated to the instances of the realization class R. Neither the creator nor the client of instances of A need to know about the existence of the realization class R. Embedding the realization in the abstract interface works well if the realization class is stable and definite, meaning that it will not be changed and need not be refined by the programmer using the component. Note that the interface class is then no longer an abstract class in a technical sense, since it no longer employs pure virtual functions.