Much of this applies to non-hierarchical taxonomies as well - the essential problem is a set of things which have (for the most part) share a group of interesting and useful properties - but which have a few exceptions to prove the proverbial rule.
An ImperfectHierarchy is one where some attribute of a base type no longer applies to a derived type. Much of typing theory assumes that derived types can be substituted for base type objects - and that every valid operation on a base type instance is also valid on subtype instances. (This is a weaker requirement than the LiskovSubstitutionPrinciple, but one that many languages, as well as O-O theory, frequently assume.)
The real world, of course, laughs in the face of these notions. Of course, the quality of your abstractions and your tools also plays a part.
Consider an example given in ObjectOrientedSoftwareConstruction - a class hierarchy representing birds. At the root is class Bird, and it has a bunch of methods for attributes of birds and things that birds do: lay_eggs(), fly(), number_of_feathers(), etc. (We'll ignore other critters that may fly or lay eggs.) These methods will be unimplemented in the base class ("abstract" in Java speak, "pure virtual" in C++ nomenclature, "deferred" in EiffelLanguage), subclasses must provide implementations. (In a language with DynamicTyping such as SmalltalkLanguage, of course, there is no need to write unimplemented methods... this is only an issue with StaticTyping.)
Individual species of bird may be subclasses - we can have class EmperorPenguin?(), WesternMeadowlark?(), RubyThroatedHummingbird?(), etc. There may be intermediate subclasses (representing the taxonomy of birds used by scientists, or representing other ways of organizing birds into subsets, etc.)
Of course, when we get around to writing the method EmperorPenguin?.fly(), we run into a little problem... penguins cannot fly. Neither can chickens, emus, ostriches, and a whole lot of other bird species.
What to do? Lots of possibilities:
- ReFactor your class hierarchy. Introduce (for example) subclasses BirdThatCanFly? and FlightlessBird? (or perhaps just BirdThatCanFly? - FlightlessBird? might not introduce any new features), and redefine all specific subclasses (of individual bird species) to subtype the correct base class, according to whether or not they are capable of flight. Potential problems with this approach: 1) You may not be able to change class Bird (part of a third-party library, for instance). 2) If you have thousands of bird subclasses, refactoring each may be too labor-intensive (unless you have VERY good tools). 3) If your language doesn't support MultipleInheritance (or if you're of the opinion that MI is ConsideredHarmful), and you already have an existing set of intermediate classes which classify birds according to some other (orthogonal) set of properties (for example, one class each for the 30-odd orders of birds recognized by biologists - a classification method which ignores whether or not birds can fly), you may not be able to refactor in this fashion.
- Redefine the base class to reflect that not all birds can fly. For example, add a query method can_fly(), list can_fly()==true as a precondition of fly(). Or, redefine fly() to return an error code or an exception if the bird cannot fly. Problems: 1) Again, you may not be able to modify the base class. 2) You may break existing code, as you are either strengthening the precondition (by declaring that fly() may not work with some birds), or weakening the post-condition (by declaring that failure to fly is now a possible result, and that the client should handle it.)
- Don't touch the base class, but make fly() return an error anyway.. Most statically-typed languages will let you hack around the declared interface of Bird.fly() in some way. C++ will happily let you violate an exception specification (a ThrowsClause?) - though you might get unexpected_exception propagated to the caller, rather than cannot_fly_exception. Java enforces throws clauses, but you can cheat by subclassing RuntimeException. In Eiffel, such cheating is easy - simply undefine the method fly in the classes representing flightless birds - though if you do so, you will create what BertrandMeyer calls a "catcall". Problems: 1) Again, you may break existing code - by returning exceptions that clients are not prepared to handle. 2) Creating "catcalls" in Eiffel might (if NoPolymorphicCatcalls? is in effect) cause perfectly legitimate client code to not compile when flightless birds are introduced to the system.
- Base your class hierarchy on capabilities needed in your problem domain, not naive mapping of existing hierarchy in other domains.. Your software has a purpose, thus your base class need the fly() method for a reason. If you find some subclass that cannot fly(), it is a good indication that your hierarchy is not reflecting you problem domain. I.e. you are studying migration flying patterns, thus all your birds must fly(), so who cares about penguins or chickens. OR you are studying migration patterns, and migration by swimming for penguins is perfectly acceptable, thus you misnamed your base fly() method, it should be called commute() instead.
Or, you could use a language with
DynamicTyping - in which no promises to allow flight are made in the base classes, and clients ought not be surprised to see
DoesNotUnderstand returned from a call to "aPenguin fly" (in
SmalltalkLanguage). After all, if you don't make a promise, you don't have to worry about breaking it...
Add bats to the mix. Bats can fly, but they aren't birds. Likewise with many species of insect, several (now-extinct) reptiles, and outside the realm of biology we have planes and helicopters. The difference is interesting to a biologist, but not to a cage maker. Cage makers don't care about the evolutionary history of an animal, they just care if it can fly. Hierarchies are often limited to specific perspectives. Don't get too hung up on finding a perfect hierarchy. Find one that works in a given context and use it. Interface inheritance is well suited to this approach. You can make "Bat" and "Swallow" implement an interface in the Flighted hierarchy instead of worrying about all mammals and aves.
See also ThereAreNoTypes, LimitsOfHierarchies
CategoryPolymorphism CategoryHierarchy CategoryOrganization CategoryIdealism