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 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 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
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.
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.
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.
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.
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.
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.