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.
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.
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.
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.
Here we will show that the IS A - HAS A test may be insufficient in some cases. First, consider the following example.
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.
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 of type there is an object of type such that for all programs defined in terms of , the behavior of is unchanged when is substituted for , then is subtype of .
// Proper Particle hierarchy class Charged: public Particle { /* ... */ }; class Uncharged: public Particle { /* ... */ };This class hierarchy is shown in the Fig. 13.6c.
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.
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:
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.
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.
|
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.