The Need for Inheritance

   


The Greek philosopher Aristotle was the first well-known taxonomist in that he began to categorize the objects that surrounded him. When taxonomists categorize part of the world, they create hierarchies of categories that move from the general to the specific.

Note

graphics/common.gif

When working with inheritance, it is useful to view the world through the glasses of a taxonomist.


For example, by using this approach, we can divide transportation vehicles into three broad categories: surface, airborne, and water vehicles, as shown in Figure 16.1; and when moving down the hierarchy, the categories become more specialized.

Figure 16.1. Specialization/generalization hierarchy for vehicles.
graphics/16fig01.gif

There are a couple of interesting things to note about the hierarchy in Figure 16.1, which can be generalized to hold for any hierarchy constructed in this manner.

  • The relatively few attributes and actions that can be attached to the general category transportation vehicle on the top of the hierarchy are shared among all categories in the hierarchy. For example any car, bicycle, sledge, and so on can be described by the attributes maximumNumberOfPassengers and approximateMaxSpeed and contain the actions MoveForward and Stop.

  • As we move down the hierarchy to the specialized categories, we can attach specialized attributes and actions that are not associated with the more general categories. For example, a car attribute such as recommendedTireSize could not be used on a surface vehicle because surface vehicles also include sledges that usually do not use tires.

  • In general, we can say the following about any two categories that are connected by an arrow: The category that the arrow points away from contains the same attributes and actions as the category that the arrow is pointing at, plus some added attributes and actions. For that reason, we can say that the category that the arrow points away from is what the arrow points to. For example, bicycle is a surface vehicle, and sports car is a car. However, the contrary is not true. A surface vehicle is not necessarily a bicycle, it could also be a car, a train, or something else.

Note

graphics/common.gif

At first, the arrows in Figure 16.1 seem to point in the wrong direction. A surface vehicle, for example, passes attributes and actions to a car. Shouldn't the directions of the arrows be reversed to illustrate this? Perhaps, but, per convention, the arrows are turned in the other direction for the following reason: Car knows about (or contains) all the attributes and actions that exist in surface vehicle, the reverse is not true.


Life Without Inheritance

Suppose that in our program we need to create objects with the attributes and functionality of a SportsCar. We also need objects with the attributes and functionality of a FamilyCar, and we need objects that have the attributes and functionality of a RacingCar (notice that these all belong to the car category in Figure 16.1). If we hadn't yet been introduced to inheritance, we could go about implementing this in two ways:

  • We could reason that all three kinds of objects are car objects and therefore share numerous class members. Consequently, we could write just one class from which all the three kinds of car objects are instantiated.

  • We could also reason that the three kinds of objects are so different that three different classes are needed a SportsCar class, a FamilyCar class, and a RacingCar class.

It is possible to make both approaches work, but each has its own set of serious drawbacks. Let's look closer at the pros and cons of these two approaches.

Approach Number 1: One Class

The SportsFamilyRacingCar class in Listing 16.1 contains fragments of source code that illustrate the way we would have to go about designing a class to represent objects that could act as the three different kinds of cars.

Listing 16.1 One Class to create SportsCar, FamilyCar, and RacingCar Objects
01: class SportsFamilyRacingCar 02: { 03:     public SportsFamilyRacingCar(string initialTypeOfCar) 04:     { 05:         typeOfCar = initialTypeOfCar; 06:     } 07:     private readonly string typeOfCar; 08:     private uint odometer; 09:     private double engineTemperature; 10:     private byte NumberOfBabySeats; 11:     private bool hasBuiltInDogNet; 15:     private string highPressureFuelPumpSystem;         ... 20:     public void StartOnBoardCamera() 21:     {             //Start transmitting back to television station.             //So people can watch race on TV. 30:     }         ... 40:     public void MoveForward() 41:     { 42:         if (typeOfCar == "SportsCar") 43:         {                 //Move medium fast 50:         } 51:         else if (typeOfCar == "FamilyCar") 52:         { 53:             //Move slowly 60:         } 61:         else if (typeOfCar == "RacingCar") 62:         {                 //Move very fast 70:         } 71:         else 72:         {                 //Invalid type of car 80:         } 81:     } 82:     public double CalculateFuelConsumptionPerKilometer() 83:     {             ... 90:     }     ... 99: } 

Note: The line numbers are somewhat arbitrary (but always increasing) because of the code fragments that are not shown. This code does not compile.

The SportsFamilyRacingCar class in Listing 16.1 is loaded with problems, but it also contains a few positive features.

Problem 1: Finding an appropriate name for the class is difficult.

Our problems already start with the naming of the class. It is more than a general car, so the name Car seems inappropriate. It is a sports car but not always; it can also be a family or a racing car. So we finally decide on the name SportsFamilyRacingCar.

Problem 2: An instance variable must be dedicated to keep track of which car type an object represents.

We need a way to determine what type of car a particular object is representing. This information is kept in the typeOfCar instance variable (see line 7), which is meant to hold one of the three strings "SportsCar", "FamilyCar", and "RacingCar" (this would better be represented by an enum, but we are trying to keep the code short). typeOfCar is assigned its value by the constructor in lines 3 6. When we start to fiddle around with instance variables that are meant to keep track of which type a particular object is supposed to be, it is usually a sign that we need to redesign our code. There should be a class for each type of object we want to create.

Advantage 1: Instance variables that are found in all three types of cars are kept in one place, so these variables do not need to be copied and are easy to maintain.

For example, all three types of cars have an odometer and an engineTemperature, as shown in lines 8 and 9. Had we written three different classes instead, as shown a little later, odometer and engineTemperature would have to be repeated in the three different classes.

Problem 3: Instance variables that are used in only one of the car types waste memory, bloat the code, and are error prone.

For example, only the family car and perhaps the sports car would need information about the number of baby seats and whether there is a built-in dog net. A RacingCar object would carry around these instance variables but never use them. Similarly, the racing car needs to carry information about the type of high-pressure fuel pumping system (to allow for fast refueling during a race) with which it is compatible (see line 15), but this is not needed for any of the other two types.

Problem 4: Function members that are only used in one of the car types bloat the code and are error prone.

For example the StartOnBoardCamera method (lines 20 30) is only relevant for a racing car. This method is needed to start its built-in camera that transmits pictures back to the television station during a televised race. However, family car objects still have to carry this method around bloating the code and making the code complex and therefore error prone.

Problem 5: Function members that carry the same name in all three car types but are implemented differently in each car type must contain complicated if...else statements to execute only the code relevant to one type when the function member is called.

The MoveForward method in lines 40 81 is used to illustrate this point. Every time MoveForward is called, it has to check which type of car the object is representing with an if...else construct to execute the corresponding piece of code. In our example, MoveForward causes a SportsCar to move medium fast, a FamilyCar to move slowly, and a RacingCar to move very fast.

Advantage 2: Function members that are identical in all three car types are kept in one place from where maintenance is easy.

The CalculateFuelConsumptionPerKilometer in lines 82 90 is an example of a method that is found in all three car types. Any upgrades to this method can be performed in one place, and the upgrades will affect each kind of car object.

Trying to represent three different kinds of car objects with one class has a couple of advantages, as the analysis in this section discussed. But overall, it is a disastrous approach because it is a burdensome task to squeeze all aspects of all three car types into one class. This is illustrated in Figure 16.2.

Figure 16.2. Squeezing class members of three different car categories into one chaotic class.
graphics/16fig02.gif

Writing three different classes is a slightly better approach but still contains inherent problems, as you will see in the next section.

Approach Number 2: Writing Three Classes

Suppose we write three different classes called SportsCar, FamilyCar, and RacingCar. The illustrative parts of the FamilyCar class are shown in Figure 16.3. I have not shown the SportsCar and RacingCar versions because the FamilyCar class amply demonstrates the problems and advantages of this approach, which are similar in the two other classes.

Figure 16.3. The FamilyCar class.
graphics/16fig03.gif

The FamilyCar class is considerably more peaceful to look at than the contorted SportsFamilyRacingCar class from Approach 1.

It is interesting to note that the parts of the code that caused problems 3, 4, and 5 in our previous analysis, give rise to the advantage sections this time, whereas the code that gave rise to the two advantages in the previous approach are causing us problems here. Let's look closer at this phenomenon.

Problem 1: Attributes and actions that belong to a general car must be copied to all three classes.

The class members that are associated with a general car (for example, odometer, engineTemperature, and the CalculateFuelConsumptionPerKilometer method) must be copied around to all three classes, as illustrated in Figure 16.4. This provides a maintenance problem. If a programmer needs to upgrade the CalculateFuelConsumptionPerKilometer method, he or she needs to perform this upgrade in all three classes. If instance variables that belong to the general car category need to be added, they must be added to all three classes.

Figure 16.4. All three classes contain copies of the attributes and actions of a general car.
graphics/16fig04.gif

Advantage 1: Unique attributes and actions are seamlessly inserted into each class.

The unique attributes and actions associated with each of the three car categories (illustrated with different shapes in Figure 16.4) are, in stark contrast to the previous approach, elegantly inserted into the class where they belong and do not interfere with any of the other two classes. Similarly, the MoveForward method does not need the cumbersome if...else statements because we already know at the time the method is being written in which type of object this method will reside.

Our assessments of the two approaches lead us to the following dilemma: Class members that are common to the three car categories are handled correctly by Approach Number 1, but cause us problems in Approach Number 2. Class members that are unique to each car category causes us problems by Approach Number 1, but are handled correctly by Approach Number 2.

Both approaches cause us problems, so let's introduce a third approach that handles both the common and the unique class members correctly, and thereby takes the best from the two approaches while leaving the problems behind.

Approach Number 3: Towards Inheritance. Leaving the Problems Behind

First, we must create a Car class and insert the common class members, as shown in Figure 16.5. Then we write three specialized classes SportsCar, FamilyCar, and RacingCar; each containing only the class members that are unique to the class. Finally, we need a mechanism that supports the following statement: Apart from the unique features held by each of the specialized classes, each of these classes must also hold the class members held by the Car class. In other words, define three new and more specialized classes by extending the existing Car class. Then let the three derived classes inherit the class members from the existing Car class for free, and add the class members that are unique for each of these new three classes.

Figure 16.5. Unique class members in different classes.
graphics/16fig05.gif

The inheritance mechanism provides support for the suggested third approach. Consequently, inheritance is called for when we need to create objects that share a group of class members as well as containing class members that are unique.

Before we look at how inheritance is implemented in C#, it is worth introducing some of the basic terminology used in connection with inheritance.

When SportsCar inherits the class members of the Car class, we say that SportsCar is derived from Car and that SportsCar is a direct subclass of the Car class. The Car class is called the base class (and sometimes a superclass or a parent class) of the SportsCar class, because it forms the base of the SportsCar class.


   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net