Recently and actually after a very long gap, I returned to reading about object oriented software and I was a tad surprised that the way the world looks at inheritance today has changed. This revelation, thanks to a good book on OOAD by John Deacon. The wine in the new bottle indeed tasted new !
My first exposure to the inheritance idea was of course the inane (it seems now) example of something like the Animal class inheritance tree, where you had Biped and Quadruped inheriting from Animal, and Dog, Cat and Horse inheriting from Quadruped. A seemingly all important behaviour for this class hierarchy was "move" for example, and the difference in the way these animals moved was modeled by the difference in the implementations of the method called move for these classes. A class that decided its way of moving was unique could override the move method of its more general parent class or be content with the implementation that the parent provided - it inherited the implementation (of the move method) of its parent if it didn't override it. Well, this line requires some qualification based on which object-oriented language we are talking about. For example in the case of C++, I am assuming public inheritance, and that the method move has either public or protected access in the Base class. Using the inheritance feature of the language this way, at once assumed that the derived class is -a-kind-of base class and also that the derived class "works-like" the base class (for whatever parts it doesn't override the way the base class works). We are using two concepts to mean what just one inheritance link represents, and that's potential trouble. The two concepts merit some more explanation.
The first concept, "is-a-kind-of" implies in all our well-known object-oriented languages that an object of the derived class can be subsituted for an object of the base class, in function parameters and return values for example. This is also called the "Liskov Substitutability Principle". So, imagine a function definition (C++ example)
A& Foo ( A obj) {......}
(I intentionally pass obj by value, just to show that substitutability applies to this pass-object-by-value case as well)
The following lines ending with a call to that function are valid
B b;
foo(b);
because the variable b which is of type B, contains an object of type B, no doubt, but an object of type B also has type A, the way the language defines inheritance i.e. when a class B inherits class A, it inherits type.
The second concept, "works-like" really means inheritance of implementation - a weak attempt at factoring. Its weak not in the efficacy of the result it achieves. We may actually have the potential to end up with very compact and non-repeated ("normalized" is the term) code in the end. But it's weak in that the likelihood that we'll get to it is low, if we assume that directly doing it is the right way to do it rather than seeing it as the end result of a different subtler way. The early methodologies were class-oriented rather than object-oriented. And we let these early methodologies lead us into following the path of implementation inheritance, which results in a lot of rework, as we realize that what methods we thought belonged to the base class actually need to be overriden in derived classes because of increased understanding of the problem to solve which require their behaviour to be nuanced and grossly or subtly different from that of the definition in the base class. If we persist through a lot of rework and finally get to the "optimum" normalized solution, we often realize that the methods that actually got factored into the base classes were quite different from what we assumed to begin with.
A problem that precedes this one is that faced with a software project, we immediately set about trying to find all the classes. This path is fraught with difficulty, because we are trying to directly hit at the generalization - the "set" of objects or the class - and determine how every object in the set will behave, without first having looked at the objects themselves and their interactions in the light of different use cases, and therefrom getting progressively better indications of what kind of classes (object sets) we are going to have.
The newer way of going about inheritance is to never look at implementation inheritance until the very end of the design phase, indeed after some coding is complete. Instead it is recommended to start design by trying to first find object types shorn of all the implementation details and even instantiability, specifically what are called Interfaces in Java and C# parlance (or pure abstract base classes in C++). A type is characterized by the messages from other objects that objects of the type can respond to. Messages are serviced by methods of the concrete classes that implement the Interface. A powerful way of finding these interfaces is to run CRC workshops based on the use cases identified during the requirements-gathering phase. It is recommended to try and keep the parameter and return types of the messages, as non-concrete as possibile, i.e. as Interfaces. This complies with the principle of keeping the code changeable or malleable, because tomorrow any concrete class (not just the one discovered in version 1 of the software) that implements the interface can be potentially used as argument or return value (by the principle of substitutability) in future software versions, with minimum impact to all the code that was coded to only use the Interfaces.
All implementation is only in the concrete leaf classes. Type hierarchies are formed as inspired by the problem domain but all super classes are kept pure abstract (interfaces) to start with. Iterfaces have names that are generic and signify a role, while concrete classes have a name that is very specific. The aim is that it should not happen that you named an Interface to be "Ship", only to later find that a submarine object can't inherit from it (because the name "Ship" is not generic enough) , even though in the problem domain they seem to share type. Another example of bad naming is that you name a concrete class as "Ship", only to find later that the problem domain also demands a "BattleShip" concrete class.
So much for now.
Signing off
- Sachin S.