next up previous contents index
Next: 13.7 Structure of the Up: 13. Object Oriented Computing Previous: 13.5 Polymorphism

Subsections


13.6 More about Inheritance

Inheritance can be easily misused and this is often counterproductive. Poorly designed inheritance hierarchies lead to programs that are difficult to understand, contain hard-to-find errors, and are difficult to maintain. In this section, we give some typical examples.


13.6.1 Substitution Principle

In Sect. 13.4, Inheritance, we have seen that any instance of any derived class may be used where an instance of the base class is expected. This is sometimes called the substitution principle.

As far as we have seen, this is a syntactic rule: If you follow it, the program compiles. But we already know that for reasonable use of the inheritance, the derived class must be a specialization of the base class. Let's investigate more deeply, what it means.


13.6.1.1 ''Technical'' Inheritance

This problem is similar to the problem we have seen in Sect. 13.4.4. We have two related classes, say A and B, and class B contains all the members of A and some additional ones. Is it reasonable to use A as a base class of B?

Of course, this situation indicates, that B might be really derived from A. But this is indication only that cannot replace the IS A - HAS A test. In Sect. 13.4.4, we have seen an example that leads to object composition. Here we give another example that will be solved by inheritance.

Example 12   Consider the particles in our Monte Carlo simulation. The interaction of electrically charged particles in the detector substantially differs form the interaction of uncharged particles, so it would be convenient to split the class of all the particles into two subclasses, one for the uncharged particles and the other for the charged ones.

The class representing the charged particles contains the same data members as the class representing the uncharged particles plus the charge attribute and the methods setCharge() and getCharge() to manipulate the charge. This might lead to the idea to define the Uncharged class representing the uncharged particles and use it as a base for the Charged class representing the charged particles. These two classes will serve as base classes for the classes representing concrete particle types (Fig. 13.6a).

This class hierarchy design is incorrect and leads to problems in the program. Suppose the following two declarations:

list<Uncharged*> ListOfUncharged;
Electron e;    //  Electron is charged particle

The ListOfUncharged variable is a double-linked list of pointers to the Uncharged instances. If the Charged class were derived from the Uncharged class, it would be possible to insert any charged particle into this container of uncharged ones. The following statement would compile and run (of course, the results would be unpredictable):

ListOfUncharged.push_back(&e); // It compiles...
The reason of this problem is evident - ''the charged particle is not a special case of the uncharged particle'' (the IS A test), so this inheritance is not applicable.

Figure 13.6: Class hierarchies discussed in Sects. 13.6.1 and 13.6.2. Only (c) is correct
\includegraphics[width=10.6cm]{text/2-13/Vir06.eps}

13.6.1.2 ''Logical'' Inheritance

Here we will show that the IS A - HAS A test may be insufficient in some cases. First, consider the following example.

Example 13   We will continue with the analysis of the Monte Carlo simulation of the charged and uncharged particles. The uncharged particles may be considered as a special case of the charged particles with the electric charge set to zero. Consequently, it seems to be logical to derive the Uncharged class from the Charged class (Fig.13.6b).

However, no member of the base class may be excluded from the derived class in the inheritance process. So the derived class, Uncharged, will contain the charge attribute and both the access methods. In order to ensure that the charge is zero, we have to override the setCharge() method so that it always sets the charge value to zero,

void Uncharged::setCharge(double ch) {
  charge = 0.0;         // Parameter value not used
}
Nevertheless, this construction may fail. Consider the following function:
void process(Charged& cp){
    const double chargeValue = 1e-23;
    cp.setCharge(chargeValue);
    assert(cp.getCharge() == chargeValue);
    // And some other code...
}
This is correct behavior of the process() function: It expects a charged particle, changes its charge to some predefined value and tests whether or not this change succeeded. If the argument is really a charged particle, it works.

However, the classes representing the uncharged particles, e.g., Photon, are derived from the Uncharged class and this class is derived from the Charged class, so the following code fragment compiles, but fails during the execution:

Photon p;       // Uncharged particle
process(p);     // Assertion fails...

This example shows, that even if the IS A test succeeds, it does not mean that the inheritance is the right choice. In this case, the overridden setCharge() method violates the contract of the base class method - it does not change the charge value.


13.6.2 Substitution Principle Revised

The preceding example demonstrates that under some circumstances the Uncharged class has significantly different behavior than the base class, and this leads to problems - even to run time errors.

This is the rule: Given the pointer or reference to the base class, if it is possible to distinguish, whether it points to an instance of the base class or of the derived class, the base class cannot be substituted by the derived class.

The conclusion is, that the substitution principle is more than a syntactic rule. This is a constraint imposed on derived classes, that requires, that the derived class instances must be programmatically indistinguishable from the base class instances, otherwise the derived class does not represent a subtype of the base class.

This conclusion has been originally formulated by Liskov ([4,5]) as follows:

What is wanted here is something like the following substitution property: If for each object $ o_1$ of type $ S$ there is an object $ o_2$ of type $ T$ such that for all programs $ P$ defined in terms of $ T$, the behavior of $ P$ is unchanged when $ o_1$ is substituted for $ o_2$, then $ S$ is subtype of $ T$.

Example 14   Let's finish the charged and uncharged particles problem. We have seen that the Charged and Uncharged classes may not be derived one from the other. To avoid both kinds of problems, it is necessary to split the hierarchy and to derive both classes directly from the Particle class:
 // Proper Particle hierarchy
 class Charged: public Particle { /* ... */ };
 class Uncharged: public Particle { /* ... */ };
This class hierarchy is shown in the Fig. 13.6c.

13.6.3 Inheritance and Encapsulation

In this subsection we demonstrate that the inheritance may lead to significant violation of the encapsulation, which may cause problems in the implementation of derived classes. We start with an example.

Example 15   The particles interact in the detector in different ways. Some of the interaction represent events that are subject to our investigation and need to be logged in the result file and further processed. (This means to record the particle type, energy, coordinates of the interaction etc.) But the events may appear in groups, so the ResultFile class will contain the methods LogEvent() and LogEventGroup(). The latter will get the vector containing data of several events as an argument. Suppose that both these methods are polymorphic.

At some later stage of the program development, we find that it is necessary to be aware of the total count of recorded events. The actual implementation of the ResultFile class does not support this feature and we cannot change it, e.g., because it is part of some program library.

The solution seems to be easy - we derive a new class, CountedResultFile, based on the ResultFile. The implementation could be as follows:

class CountedResultFile: public ResultFile
{
public:
    virtual void LogEvent(Event *e)
    {
        ResultFile::LogEvent(e);
        count++;
    }
    virtual void LogEventGroup(vector<Event*> eg)
    {
        ResultFile::LogEventGroup(eg);
        count += eg.size();
    }
private:
    int count;
};
The overridden methods simply call the base class methods to log the events and then increase the count of the recorded events.

It may happen that we find that the LogEventGroup() method increases the count of recorded events incorrectly: After the call

 LogFile *clf = new CountedLogFile;
 clf -> LogEventGroup(eg);   // (*)
the count value increases by twice the number of the events in eg.

The reason might be that the implementation of the LogEventGroup() method internally calls the LogEvent() method in a loop. This is what happens:

  1. The (*) statement calls the LogEventGroup() method. This is a polymorphic method, so the CountedResultFile::LogEventGroup() method is called.

  2. This method calls the base class LogEventGroup() method.

  3. The base class method calls the LogEvent() method in a loop. But because these methods are polymorphic, the method of the actual type, i.e., the ComputedResultFile::LogEvent() method is called.

  4. This method calls the base class method to record the event and increases the count of events. After that it returns to the CountedResultFile::LogEventGroup() method. This method increases the event count once again.

To implement the derived class properly, we need to know that the ResultFile::LogEventGroup() method internally calls the ResultFile::LogEvent() method. But this is an implementation detail, not the part of the contract of the methods of the ResultFile class.

13.6.3.1 Solution

This problem may easily be avoided by using interfaces and object composition (cf. class diagram in Fig. 13.7); it is necessary to use another design of the ResultFile class, as well as another design of the CountedResultFile class.

Figure 13.7: Class diagram of the correct design. Only interface methods and corresponding attributes are shown
\includegraphics[width=9.5cm]{text/2-13/Vir07.eps}

First we design the ResultFileInterface interface as follows:

class ResultFileInterface {
public:
     virtual void LogEvent(Event *e) = 0;
     virtual void LogEventGroup(vector<Event*> eg) = 0;
};
The class ResultFile will implement this interface:
class ResultFile: public ResultFileInterface {
 // Implementation as before
};
Now, CountedResultFile may be designed as an independent class that implements the ResultFileInterface and uses the ResultSet as an attribute:
class CountedResultFile: public ResultFileInterface {
public:
    virtual void LogEvent(Event *e)
    {
        rs.LogEvent(e);
        count++;
    }
    virtual void LogEventGroup(vector<Event*> eg)
    {
        rs.LogEventGroup(eg);
        count += eg.size();
    }
private:
    int count;
    ResultSet rs;
};

The problem may not appear, because the CountedResultFile is not derived from the ResultFile now. Nevertheless, they may be treated polymorphically, i.e. instances of the CountedResultFile may be used instead of instances of the ResultFile, if they are used as instances of the ResultFileInterface interface.


next up previous contents index
Next: 13.7 Structure of the Up: 13. Object Oriented Computing Previous: 13.5 Polymorphism