The Truth about Multiple Inheritance
October 2006
Most Eiffel programmers probably don't need much convincing on this month's
topic, but it is the victim of misconceptions so widely held as to justify a
short clarification.
Multiple inheritance has long been the target of a FUD campaign (seeding Fear,
Uncertainty and Doubt) which, one must say, has been pretty effective. For fun I
sometimes ask a new audience to raise their hands if they have been successfully
convinced that multiple inheritance is "bad" in some sense; whether they are
students or engineers, the show of hands is always impressive.
There are, I think, two original reasons. The first is that multiple inheritance
is hard to implement efficiently; Eiffel was made possible when (among other
things, but this was a key factor) we found a way, back in 85, to implement
multiple inheritance so that dynamic binding would take constant time. It was
not deemed possible at the time; both Simula and Smalltalk had single
inheritance. Already then there were nasty comments about multiple inheritance
being messy, the unmistakable sign of bad design etc. We always felt that this
was an excuse; if you can't implement something, vilify it. The experience of
Eiffel quickly showed that multiple inheritance was useful and that excellent
designs could take advantage of it. I should add here that the presence of
multiple inheritance in Eiffel had a precise source: extensive experience with
Simula 67, which only had single inheritance, had demonstrated a contrario that
we couldn't do a decent job of modeling systems -- the prime task of programming
in the object-oriented view -- without multiple inheritance.
The next episode was multiple inheritance as present in C++ through -- I think
one can say without too much fear of contradiction -- not the most lucid
language mechanism ever designed. In particular, multiple inheritance just
doesn't go well with the C++ idea, also retained in Java and C#, that a
constructor (creation
procedure) must start its execution by calling the constructor of its parent. As
soon as you require this you are ruling out multiple inheritance, since with two
parents or more there's no good reason for executing a particular parent's
constructor before the others'; and the situation becomes even more hazy when
these parents have a common ancestor -- repeated inheritance, the so called
"diamond" structure. Hence complicated rules that probably about twelve people
in the world understand, and fewer remember.
That mechanism gave further ammunition to the attack on multiple inheritance,
but the arguments are about the C++ design more than about the concept per se.
Eiffel's design is not subject to most of these arguments. In particular, Eiffel
doesn't force the creation procedures of a class to use those of its parents, as
there is no reason for such a rule (each class can initialize its instances the
way it wants to); and the fundamental structure of an Eiffel class, a one-to-one
mapping between feature names and features -- no overloading -- naturally leads
to the renaming mechanism which removes any conflict between identically named
parent features. Finally, repeated inheritance is addressed through the
sharing/replication rule, based on a very simple criterion in line with this
principle: a feature multiply inherited under one name is a single feature; a
feature inherited under two different names is replicated. The "select"
mechanism removes any remaining ambiguity for dynamic binding. One should add
that it's only with the ECMA standard that all the details of the mechanism have
been ironed out (see the standard text at http://www.ecma-international.org/publications/standards/Ecma-367.htm),
which also introduces the useful complementary notion of non-conforming
inheritance, but the essentials have been there from the start.
The new form of the anti-multiple-inheritance prejudice is fueled today by the
presence of "interfaces" in Java and C#. The new accepted wisdom is that it's OK
to be limited to single inheritance from classes, as long as a class can inherit
from several interfaces. But this is just as wrong and restrictive.
An interface is, in Eiffel terms, a class that's completely deferred: it only
specifies features, but no implementation for any of them. Note in passing that
in the languages mentioned the meaning of "specifies" is much weaker: one of the
benefits of Eiffel's deferred features (features specified but not implemented)
and deferred classes is that they can have contracts even without the
implementation; a deferred feature may have a precondition and a postcondition,
and a deferred class may have an invariant. Without contracts, "specified"
simply means having a type signature, which is not much. But there are even
worse problems.
Forcing an interface -- as used in multiple inheritance -- to be completely
deferred means it can't have any algorithm at all. A typical case of where this
rules out useful applications is a class like COMPARABLE, describing objects
comparable through a total order relation. It seems at first like a good
candidate for an interface: the feature "<" -- alias for `less_than' -- is
deferred, to be implemented in descendants describing specific kinds of ordered
values (integers, reals, tennis players, whatever). But now you will also want
">". There's only one reasonable definition for a > b once you have "<":
Result := (b < a)
This is an implementation! Same thing for "<=":
Result := (a < b) or (a.is_equal (b))
and ">=". So what you do in Eiffel is to make COMPARABLE deferred with the
feature "<" deferred, and the other three effective (non-deferred, i.e.
implemented) with the implementations as shown for two of them.
Then any descendant class that needs an ordering property, such as TENNIS_PLAYER,
inherits COMPARABLE and implements "<" to describe the particular order relation
of interest to you. The effective features -- here, the other three
-- follow automatically. End of story. Thanks to multiple inheritance,
TENNIS_PLAYER can inherit from anything else it wants to, for example a class
PLAYER describing players in any game, or CLUB_MEMBER etc.
With interfaces you cannot do that. Any descendant of COMPARABLE will have to
implement all the features. It's code duplication, with all the standard
problems: it's tedious to program; worse yet, it's error prone because there is
no way to avoid a mistaken implementation of ">", "<=" or ">=" in some
descendant. You don't even have contracts to try to enforce some discipline on
such redefinitions in descendants.
The example of COMPARABLE is important because it's typical of a class meant to
be used through inheritance; it provides just a facet of functionality --
ordering -- to be combined with many others, so most of its descendants will
typically need other parents as well. But it also provides some default
implementations for some of the functionality. You can't do this with
interfaces.
Here is another example, drawn from the well-known Observer pattern. Never mind
that you don't really need this pattern in Eiffel, because you can do better on
every count thanks to agents (you can find a paper on this at http://www.inf.ethz.ch/~meyer/publications/events/events.pdf,
and the supporting Event Library). Let's just consider the pattern as it is
usually described. "Publisher" objects must be instances of classes inheriting
from a high-level class PUBLISHER, and similarly for "subscriber" objects with a
class SUBSCRIBER. A typical publisher object is a GUI element which, when it
detects that some event such as a mouse click has occurred, notifies all the
subscribers, of which it keeps a list, here called `subscribers'. This is done
through a procedure `publish' involving a loop:
publish
-- Notify all subscribers that event has occurred.
do
from subscribers.start until subscribers.after loop
subscribers.item.update
subscribers.forth
end
end
On the subscriber side, there is a procedure `update', which describes what
subscribers of the given type must do in response to such an event. At the level
of class SUBSCRIBER, the procedure is deferred; specific descendants implement
it, each in the desired way. Then the above code works beautifully thanks to
dynamic
binding: it calls the right `update' depending on the particular type of each
subscriber object, obtained through `item'. But SUBSCRIBER should not be just an
interface; for example it needs a procedure
subscribe (p: PUBLISHER)
-- Subscribe me to `p'.
do
p.subscribers.extend (Current) -- i.e. add me to p's
-- subscribers list
end
which is as effective as they get (and could use a contract, I know, but this is
just a sketch with the essentials).
Like the descendants of COMPARABLE in the preceding example, the descendants of
SUBSCRIBER are ordinary application classes; along with SUBSCRIBER they need
their specific parents. And it's just the same for the descendants of PUBLISHER:
many of them will need to inherit from other classes as well. But PUBLISHER,
like SUBSCRIBER, cannot be completely deferred; true, it has some deferred
features, but also the effective procedure `publish', with its implementation as
given above, the heart of the Observer pattern.
Yet another kind of example, familiar to Eiffel programmer, is the "compound"
structure such as COMPOUND_FIGURE which inherits from both FIGURE and LIST
[FIGURE]. An amusing aspect of this example, which I have often used in
lectures, is that people who have been somehow told since childhood that
"multiple inheritance is bad" and that it usually signals the even worse vice of
"implementation inheritance" initially
exclaim: "but the other link should not be inheritance!". Then I ask which one
of the two is the improper one, and it turns out half of the audience thinks
it's FIGURE and the other half that it's LIST! In fact, both are perfectly
legitimate inheritance links. A composite figure is a figure, to which you can
do all that one does to figures -- show, hide, scale, move, rotate and the whole
nine yards --; and it's a list, to which you can do all that one does to lists:
add an element, remove an element, change the order of elements, iterate an
operation on all the elements (like `publish' did to `subscribers' above with
`update'). Classes like FIGURE and LIST cannot, again, be replaced by
interfaces. If you only have single inheritance, you end up duplicating
considerable amounts of code.
In Eiffel programming you encounter such examples daily and don't think twice
about using multiple inheritance. Interfaces don't do the job.
The notion of interface results, in my opinion, from a serious misunderstanding
of object-oriented principles. A key property of object technology is the need
for a continuum in the description of systems, from the most abstract view --
requirements- oriented, user-centered -- to the most concrete (at the
implementation stage), and everything in between. Deferred classes and
inheritance support this process of constant refinement, since they enable you
to capture at each level exactly what you know, and leave open what you don't
know, with contracts at your disposal to constrain future refinements to the
desired properties. Having just interfaces and classes destroys all this, since
it causes an all-or-nothing choice: implement either nothing or everything.
It's a very crude view, a move backwards to a time when software developers had
to reconcile high-level specifications devoid of practicality and low-level
programming languages devoid of abstraction; it's the negation of what we have
learned about the need for a seamless development process, and it is amusing but
also a bit sad to see such a disqualified concept presented as the latest
innovation in software design.
-- Bertrand Meyer |