There is almost never a reason to use more than three layers of inheritance.
This first layer would be an AbstractBaseClass that abstracts a protocol. Don't jump to creating an AbstractBaseClass first. It should come from experience. Much of the time there will just be a concrete implementation with no abstraction or derived classes.
The second layer would be a complete implementation of the AbstractBaseClass or a partial implementation that is expected to be specialized further.
The third layer is the complete implementation of the partial implementation at the second layer. You should never need to derive from this layer. Instead, backup and make a new second layer class.
The advantage of this architecture is all classes can work in terms of the abstract base class. With the second layer developers can make use of a fairly functional base class and the system is easily extended from here. The third layer is the layer developers will spend most of their time in. Three layers is not too deep to understand, yet allows almost all solutions to be expressed in an extensible manner because of the abstract base classes.
I would go a step further and say there should be a maximum of two layers of inheritance. In cases where I have seen three levels of inheritance, the code is always improved by converting it to two levels.
I think it depends what you put in the layers. Putting generic behaviours/algorithms/protocols in the second layer is very powerful. Developers then just have to specialize methods used by the algorithms. This saves a lot of duplicate code. Of course, if you don't make your first layer abstract then this is a two layer pattern.
I am not sure what you mean by "generic behaviours/algorithms/protocols." If these are not exposed externally, then they should be included as an internal utility class or classes. These probably bind to certain data types. This sounds like something that would be better handled by composition than inheritance.
There are advantages to implement the "generic behaviours/algorithms/protocols" in the second layer while the third layer wil specialize those methods described in the TemplateMethodPattern. An example:
class AbstractView {
public: virtual void display() = 0;};
class SingleObjectView : public AbstractView {
public: virtual void display() { preDisplay(); displayObject(getObject()); postDisplay(); } virtual void preDisplay() = 0; virtual Object getObject() = 0; virtual void postDisplay() = 0;};
class DogView : public SingleObjectView {
public: virtual void preDisplay() { grabMyDog(); } virtual Object getObject() { return myDog; } virtual void postDisplay() { letGoOfMyDog(); }};
Refactoring a deeply nested hierarchy to one with a maximum of three layers would probably involve use of StrategyPattern, BridgePattern, StatePattern, and CollapseHierarchy.
Should the AbstractBaseClasses in the top layers be always be purely abstract classes (like an interface in JavaLanguage or DotNet)?
I think if your top class is not concrete it should be purely abstract. This is in theory. Many times I do add some functionality.
I always have trouble finding reasonably short descriptive names for the second layer classes since these classes neither define pure interface specifications nor do they completely implement an existing interface, i.e. they neither define nor fully implement a concept but live in the nether regions land of implementation details. I usually give up at some point and just give them a VeryLongDescriptiveNameOftenWithTheWordMixInUsedSomewhereInside.
I don't buy it. My gut reaction is, "Why should you even know?" For example...
In a couple of place, BertrandMeyer describes putting together a window system for the EiffelLanguage as an example of MultipleInheritance. One of the core classes, Window, inherits from both Rectangle (a window is a chunk of screen real estate) and Tree (windows form a hierarchy, a la X) -- but the Rectangularity isn't important for this discussion.
Now, it turns out that Tree inherits from LinkedList (you can treat a tree as a collection of its children), which in turn is a List. List->Linked_List->Tree->Window is four deep, and you probably want to subclass Window for your own application (Menu, AnnoyingPopUpQuestion, etc.).
Back to the question -- Why should you even know that Window has this deep hierarchy, unless you read the source? Windows have some sort of subwindow accessor, which happens to be the same as (but called something different from) Tree's children accessor, which happens to be the same as (but, again, of a different name from) List's element accessor. But you don't care about that, you simply care that you can find out your window's subwindows. The inheritance graph, from the application programmer's perspective, is mere implementation detail. Likewise, the inheritance graph of Tree is mere implementation from the perspective of the author of the window system.
-- BillTrost
You can still mixin at any level. A mixin doesn't increase depth.
I think I confused the issue by mentioning the mixins -- I've rewritten that to hopefully make that less prominent.
Inheriting from link means your object can only be on one list though.
Untrue. In C++, the author of Tree should make the inheritance from Link private. In Eiffel, Tree renames the methods it gets from List, so mixing in another List would allow the Tree to appear in another list. This is consistent with the "Why should you even know" principle.
Maybe i don't understand how this works. A link often has a prev and next pointer. Regardless of private inheritance that means you can only be on one list a time.
Oh goody, I get to show off my lack of C++ knowledge. A little repeated inheritance:
struct Link {Link *prev, *next; Link(Link *p, Link *n): prev(p), next(n) {}}; struct L1: Link {L1(Link *p, Link *n): Link(p, n) {}}; struct L2: Link {L2(Link *n, Link *p): Link(p, n)}; struct DoubleLink: L1, L2 { DoubleLink(Link *p1, Link *n1, Link *p2, Link *n2): L1(p1, n1), L2(p2, n2) {} };DoubleLink is both a L1 (a Link), and an L2 (a Link). L1::next can be manipulated without affecting L2::next.
''Anyway, the argument is that deep hierarchies are very complicated to debug and understand. It's not 100%, but that's why it is just a rule.''
See MixIn, MeaningfulName, DontNameClassesObjectManagerHandlerOrData, ProgrammersThesaurus, LimitsOfHierarchies.