6.2 Using Inheritance to Share Implementation


Using inheritance to share implementation is illustrated by example in this section, where two classes sharing an implementation is described. First, the complete implementation of each class is shown, assuming that each was developed as an independent class. Then, a base class that captures those aspects of the implementation comment to the two classes is defined. Finally, the original classes are then reimplemented using inheritance. Objects of the two new derived classes will have the same interface and behavior as objects of the original classes.

The definition of the Number class is given below. This class captures the abstraction of an integer value that can be incremented by one, without limit. Objects of this class can be used to count the occurrence of some event. The constructor specifies the starting value for a Number object, zero by default. The Next() operation increments the value of the Number object by one. The current value of the Number is returned by the Value() accessor method. The Number class also provides a facility to display its current value to the user in a TextBox as a character string. The ShowIn method creates an association with a TextBox where the Number's value should be displayed. The Show() method tiggers the actual displaying of the current value in the TextBox. Finally, the Reset() method allows the user to provide a new value for the Number. This method assigns a new value to the Number by converting to an integer the character string read from the TextBox.


The Number Class
class Number {
  private:
     int value;
     TextBox* textBox;
  public:
         Number(int initValue = 0);  // start at initValue
    void ShowIn (TextBox& tbox);      // place to display value
    void Show();                      // display current value
    void Next();                      // increment
    void Reset();                     // get new value from user
    int  Value();                     // reply current value
        ~Number();                   // 
  };

The Cycler class is similar to the Number class, except that the integer value maintained by the Cycler is constrained to a range determined by the base of the Cycler. The base value is specified when the Cycler object is constructed. The value of a Cycler object must always be in the range from 0 to base-1. Cycler objects are useful for counting events that are numbered cyclically, like seconds in a minute (0-59) or hours in a day (0-23). Like a Number, the Cycler also contains methods for creating an association with a TextBox that will display its current value in the TextBox and for obtaining a new value from the user. The definition of the Cycler class is shown below.


The Cycler Class
 class Cycler {
    private:
      int value;
      int base;
      TextBox* textBox;
    public:
           Cycler(int b = 10);        // modulo base b
      void ShowIn (TextBox& tbox);    // place to display value
      void Show();                    // display current value
      void Next();                    // increment
      void Reset();                   // get new value from user
      int  Value();                   // reply current value
          ~Cycler();                  // clean up
   };

The similarities in the Number and Cycler classes are evident in their definitions, and include both the data that each maintains and the code for some of their methods. Specifically, the similarities are:

       Data:
                value     : an internal integer value
                TextBox*  : a pointer to a TextBox

        Code:
                ShowIn    : provide a TextBox where value is displayed
                Show      : display the current value in the TextBox
                Reset     : use the TextBox to get new value from user
                Value     : returns the current internal value

Notice that the Next method is not similar: its implementation distinguishes the classes from each other. Notice also that the constructors and destructors are not the same. Because the constructor and destructor each take the class name, it is not possible that they be shared among classes with different names.

Identifying the similarities among classes is a process of generalization resulting in a new class. The new class is referred to as the base class (also sometimes called the superclass or the parent class). The classes over which the generalization is made are referred to as the derived classes (or subclasses or child classes). The base class is named so as to reflect its role as a generalization. In the example, the generalization is formed over classes that have a numeric property (Numbers and Cyclers have numeric values) and a property of being displayable to the user. Thus, one name for the generalized (base) class is DisplayableNumber; other names such as InteractiveNumber, GUIValue, and VisualNumeric are possible.

The classes over which the generalization is formed are viewed as specializations. Each of these classes contains distinctive properties (data and/or methods) that are beyond the similar properties captured in the base class. These distinctive properties also differentiate a class from other classes that are specializations of the same generalization. Thus, both Number and Cycler are specializations of DisplayableNumber, each is a particular kind of DisplayableNumber. The specializing property in this example is the Next() method. The Next() method is not part of the generalization because it has a different meaning in the two classes.

A generalization-specialization hierarchy is used to organize the generalized class and its specializations. The generalization-specialization hierarchy for the example is shown below. The class for a generalization is placed above the classes of its specializations. Each class in the hierarchy is represented by a class diagram with three sections: the top section gives the name of the class, the middle section gives the data of the class, and the bottom section gives the methods of the class. A line is drawn between a generalization (in this case DisplayableNumber) and its specializations (Number and Cycler).


Generalization-Specialization Hierarchy



Inheritance is the object-oriented programming form of a generalization-specialization hierarchy. The term inheritance is used because each derived class inherits from the base class the base class's code and data and need not be repeated in the definition of the derived class. For example, since the Number class inherits from the DisplayableNumber class, the Number class implicitly has all of the data and methods of the DisplayableNumber class. The only elements defined in the Number class are the specializing features that make it distinctive. In this case only a Next() method is added by the Number class. This method is distinctive in the sense that the generalization does not contain a Next() method and the Next() methods of the other specializations of DisplayableNumber are different from the Number class's Next() method. Similarly, the Cycler class inherits all of the data and methods of the DisplayableNumber class and adds to that inherited data a new data element, the base of the Cycler, and an additional method, Next().

The programming advantage of inheritance is that the definition and implementation fixed in the base class does not have to be repeated in the derived classes. Clearly, this reduces the amount of coding that has to be done to add a new derived class. Perhaps more importantly, because the base class may have existed for a long period of time its implementation is, therefore, more likely to have been tested and debugged. In this way inheritance aids in the more efficient development of more reliable software systems.

Object Structure

The structure of an object instantiated from a derived class may be thought of as a series of layers - each layer corresponds to one level in the inheritance hierarchy. For example, the structure of a Cycler object is shown below. Cycler objects have two layers, one corresponding to its base class, DisplayableNumber, and one layer corresponding to its derived class, Cycler. Each layer adds the data and methods defined by the corresponding class definition. Thus, the object possesses the union of all of the methods and data of all of the classes from which it directly or indirectly inherits.


Logical Structure of a Cycler Object

The layered object structure helps to explain why a derived class object is able to respond to methods that are defined and implemented in the base class. An example of this is shown in the following code:

    Cycler octal(8);
     ...
     TextBox tbox(...);
     ...

     octal.ShowIn(tbox);        // apply base class method
     octal.Next();              // apply derived class method
     octal.Show();              // apply base class method

As seen in this code, a Cycler object is able to perform those methods defined in the derived class (i.e., Next) as well as those defined in the base class (i.e., ShowIn and Show).

Multiple Levels of Inheritance

A derived class can itself be a base class whose implementation may be shared by classes derived from it. In this manner, inheritance extends over several, and perhaps many, levels. The example using DisplayableNumber and Cycler had only two levels, but more extensive use of inheritance to share implementations is possible and even common.

To illustrate multiple levels of inheritance, consider a JumpCounter that can be incremented by 1one or by an arbitrary amount. Such an object might be used to show the current length of a file where the length of the file may be changed by appending a single character (increment by one) or by appending an entire string (increment by the length of the string). Alternatively, the JumpCounter may represent the current page number in a document. In some cases the user turns the pages one at a time. In this case the JumpCounter should be able to increment its value by one each time its Next method is called. In other cases, the user may jump ahead many pages, by following a link or reference. In this case the JumpCounter should be able to change by an arbitrary integer amount.

The inheritance needed to define a JumpCounter is shown in the diagram below. Because the Number class has all of the functionality necessary to implement a JumpCounter except the ability to increase the internal value by a given amount, the JumpCounter class is derived from the Number class. Recall that the Number class is itself derived from the DisplayableNumber class.


Multiple Levels of Inheritance


Notice that the JumpCounter overloads a method that is defined in a class from which it inherits. The Next() method is defined in the Number class and the JumpCounter defines an overloading of the method, Next(int). In general, many overloadings are possible, either defined within a single class or accumulated across several levels of inheritance. An overloading that is introduced in a derivd class should maintain the conceptual similarity of the use of the name of the overloaded method. In the case of the JumpCounter, the two variants of the Next method have conceptual similarity; the two methods provide alternative ways of increasing the value of the Number. By contrast, the conceptual similarity would be violated by introducing an overloading of the Next method that had the meaning "this is the amount by which the next increment should increase the value." Such an overloading weakens the meaning of Next by confusing whether it applies to the current operation or to a future operation.

The essential point is that a JumpCounter object responds to all methods of its immediate class (JumpCounter) as well as to all of the methods of its ancestor classes (Number and DisplayableNumber). This is illustrated by the following code:

    JumpCounter  fileLength;
     TextBox      lengthDisplay(Location(100,100), Shape(50,20));
     ...
     fileLength.ShowIn(lengthDisplay);     // uses method in DisplayableNumber
     fileLength.Show(lengthDisplay);       // uses method in DisplayableNumber
     ...
     fileLength.Next();                    // add 1 using method in Number 
     ...
     fileLength.Next(50);                  // add 50 using method in JumpCounter
     ...
     fileLength.Reset();                   // uses method in DisplayableNumber

The layered nature of the object structure again helps to explain the behavior of the JumpCounter object in this code. A JumpCounter object has three layers, one for the derived class, JumpCounter, a second for the base class of JumpCounter, Number, and the third for the base class of Number, namely DisplayableNumber.


Object Structure of a JumpCounter Object

In general an inheritance diagram may form a deep and/or broad tree structure. Each node in the tree denotes a class that inherits from its parent (base class) and also serves as a base class for its descendents. The exercises below, when completed, will build an inheritance graph that looks like the one shown in the figure below.


An Inheritance Hierarchy of Displayable Numbers



Designing a new derived class by inheritance commonly makes the introduction of a new variation or extension of an existing class easier than would be reimplementing the class entirely from scratch.

It will become clearer as more is learned that designing the "right" inheritance structure is both important and difficult. Much of the effort in object-oriented programming is invested in the search for good inheritance structures.

 

  1. The class DownCounter maintains an integer counter whose nonnegative value, given initially by a constructor argument, is decremented by one on each call to its Next operation until the value reaches zero. Upon reaching zero, subsequent calls on Next will have no effect; the value remains at zero. The value of a DownCounter object can be displayed in a TextBox. Using an inheritance diagram, show how the DownCounter class is defined by inheritance from DisplayableNumber.

  2. The class UpDownCounter maintains an integer counter whose nonnegative value, given initially by a constructor argument, is incremented by one on each call to its Next operation. The UpDownCounter class also has a Previous method that decrements the counter's value by one. The counter's value cannot be decremented lower than its initial value. The value of a UpDownCounter object can be displayed in a TextBox. Using an inheritance diagram, show how the UpDownCounter class is defined by inheritance from Number.

  3. The class BiCycler is to be defined that maintains an integer counter that is incremented modulo the base given by the constructor. The BiCycler class also has a Previous method that decrements the counter's value by one. When the internal value is zero, the Previous operation sets the internal value to the largest possible value (i.e., base-1). Using an inheritance diagram, show how the BiCycler class is defined by inheritance from Cycler.

  4. The class SwitchCounter maintains an integer counter that is incremented or decremented by the Next method depending on the current direction of the SwitchCounter. Initially the SwitchCounter is directed up. When up, the value is incremented by one when Next is called. When in the "down" direction the value is decremented by 1 when Next is called. The Switch method changes the direction to the opposite of its current setting. Using an inheritance diagram, show how the SwitchCounter class is defined by inheritance from DisplayableNumber.

  5. The class BatchCounter maintains an integer counter that is incremented by one only when the Next operation has been called n times. The value of n is given as a constructor argument. This class counts batches of Next operations. It may be used, for example, to increment a one-minute counter only after its Next method has been called sixty times by a one-second Clock. Using an inheritance diagram, show how the BatchCounter class is defined by inheritance from DisplayableNumber.

 




©1998 Prentice-Hall, Inc.
A Simon & Schuster Company
Upper Saddle River, New Jersey 07458

Legal Statement

 

 

 

ÿ