FAQ 8.04 Is an Ostrich a kind-of Bird?
Suppose a system uses a base class Bird with member functions altitude() and fly(), where fly() promises that the bird's altitude() will be greater than zero. class Bird { public: int altitude() const; // PROMISE: Returns this Bird's current altitude virtual void fly(); // PROMISE: altitude() will return a value > 0 virtual ~Bird(); }; Bird::~Bird() { } Can an Ostrich class inherit from Bird? Unfortunately, ostriches can't fly (that is, the altitude of an ostrich remains at zero when it tries to fly). Despite its intuitive appeal and regardless of how many other member functions are inheritable, deriving Ostrich from Bird causes problems. The problems stem from the inherent incompatibility of the following statements.
At least one of the statements must be false; they cannot be simultaneously satisfied. We examine the impact of invalidating each of the three statements. Note that there is no correct solution; the particular situation dictates whichever is best (or perhaps least bad).
There are two common traps that people typically fall into. The first is to suggest the following as a fourth alternative: create a class such as AnyBird, and inherit both Bird and Ostrich from that new base class. Although there is nothing wrong with this alternative, it is not a fourth alternative. This is merely a repackaging of alternative 2 since it does not try to make Ostrich inherit from Bird and does not try to make Ostrich substitutable for Bird. The second common trap is to assume that there must be some universal right or best answer for the problem. In reality, it is critical that designers know all three of the tools that can get them out of an improper inheritance situation. In other words, when an improper inheritance situation comes up, make sure to try all three possibilities do not dismiss any of them ahead of time. For example, although the third choice ("make the ostrich fly") seems undesirable in principle, there are times when it may be the most practical. For example, if there is enough code that has already been written based on the promise in the base class, then it will be very expensive to change the base class's promise, ruling out alternative 1. Furthermore, if it is important to be able to pass the derived class into that preexisting code, alternative 2 is ruled out, leaving only alternative 3. If the problem is caught early enough in the software development life cycle, clearly it is still possible to weaken the base class's promises since there is not yet a lot of existing code that relies on the strong promises made by the base class. So one of the messages here is to think rather than hack. If a problem like this is detected early enough, the cost will be minimal; if it isn't caught until late in the game, the consequences can be pretty grim. Do not confuse substitutability with subset. Even though the set of ostriches is a subset of the set of birds (every ostrich is in the set of birds), this does not imply that the behavior of Ostrich is substitutable for the behavior of Bird. The substitutability relation has to do with behavior, not subsets. When evaluating a potential inheritance relationship, the important factor is substitutable behavior, not subsets (see FAQ 7.08). |