[Prev][Next][Index][Thread]
Re: MI: why?
jt wrote:
>
> Now this is interesting. I try to learn by other's mistakes and from other's
> advice, and I have heard a lot of condemnation of multiple inheritance.
> Consequently I have carefully avoided it when it is present (C++ - may it rot) and
> not missed it when it isn't there (delphi - but then delphi is primarily used by me
> as a database front end, so it's a bit irrelevant). I don't condemn MI and I would
> rather a language provided it, so I could ignore it, rather than be denied it by
> someone else's choice.
Multiple inheritance does present some difficult issues, and the C++
implementation is particularly filled with potential traps. (Thus it
fits right in with the rest of the language!) It's interesting to note
that the only OO languages without MI are either older, early generation
OO (Smalltalk, Objective-C, Object Pascal) or poorly designed kludges
(Java). It's interesting to look at the languages that implement MI and
note that all modern decently designed OO languages provide it:
CLOS, Dylan, C++, Eiffel, Sather (and basically ALL university/research
OO languages that I know of).
Of course, my argument here is somewhat circular, since I claim that a
good design requires MI I could be accused of just classifying according
to my prejudices. But really, compare the lists: what do you think?
(Beta is an exception. It looks nice to me, yet is SI. Where's the
disconnect?)
> I can imagine rare cases where it might be valuable but I haven't personally come
> across them yet. I am wondering if MI from purely abstract base classes is at all
> bad. I don't like the feel of MI from non-abstract base classes - it doesn't feel
> right.
In C++ this might be a good guideline. (Or more precisely, inherit from
at most one non-abstract base class.) But it is an artifact of the way
C++ implements MI. (In fact, maybe the rule is inherit from at most one
non-virtual base class. But it's C++; everything is a special case and
all rules are made to be broken!)
MI is different enough among implementations that rules for its
successful use vary widely. For instance, in Dylan one does not have to
worry about getting multiple instances of the same slot via different
inheritance paths, but one does have to be very careful about naming
slots in order to avoid having different slots with the same name
merged. While in C++ one can get multiple copies of the same slot, but
name conflicts between "different" slots with the same name can be dealt
with pretty easily.
> Please tell me more about why MI is good/bad/misused/whatever. I feel that I may be
> missing out on a valuable facility.
This is hard. Think of recursion. The standard example is the recursive
version of factorial, which is practically worthless. It does show that
you can use recursion instead of iteration. It can show that with tail
recursion iteration is not needed for efficiency reasons. But it
certainly does not give a clue at all as to what you can do with
recursion that you cannot do with iteration (assuming you are not
allowed to cheat by using arrays to implement your own stack of values
within the loop). Now, quick, think of an example that you can describe
in a paragraph or two that demonstrates the full power of recursion.
Not easy is it? Now multiply the difficulty by 100 and you have an idea
how hard it is to motivate the need for MI. To put it another way, I
know of no small or compact example that really needs MI. In fact, I
suspect that it is in the nature of MI that no such example can exist;
MI is ONLY useful for fairly large class graphs.
Let me resort to anecdotes for a moment, comparing 2 frameworks for the
Mac. Think Class Library (for Mac) was a (mostly) SI design. PowerPlant
was fully MI using mix-in classes extensively to compose concrete
classes. TCL was fairly mature (though I was working with a new and very
buggy release) and had quite good and complete documentation, and a nice
class diagram poster that I kept on the wall in plain sight. I had spent
about 6 months using the TCL framework every single working day. Then I
took a look at PowerPlant; early release, very sketchy incomplete
documentation, no complete class diagram anywhere, just portions, some
parts of the framework had no diagram at all and just a few sentences of
description. After a half day with that documentation I understood
PowerPlant better than TCL! (I not only felt that way at the end of the
day; when I later switched to PowerPlant I found that my feeling was
correct.)
Now, the question is why? MI frees you to separate your abstractions
more cleanly; the responsibilities of classes can be more coherent, the
overall number of classes can be slightly lower, and the inheritance
relationships between them more direct. Often in designing you may note
that 2 fairly large, complex (maybe "important" would be a good word?),
and unrelated classes share some small functionality that deserves its
own small class in order to allow that functionality to be reused. With
MI, you simply abstract that functionality into a mixin class and have
both original classes inherit it. With SI, you create a small class and
then have to work your way up the hierarchy until you get to a base
class that is a common ancestor of both large classes, and insert your
new class there. With MI, the two classes that need the functionality
inherit it directly, while with SI a (possibly large) number of
intermediate classes inherit functionality for which they have
absolutely no use.
Thus with MI, the classes which a given class "uses" via inheritance are
clustered tightly around the class, while with SI they get spread out.
With MI you get a more coherent design, in which clusters of classes are
MUCH more cohesive. Having learned several (4 in depth, 3 more to a
lesser extent) OO libraries I can state STRONGLY that it is much easier
to find your way around (and extend) a moderately-well-designed MI
library than ANY SI library. The TCL vs PowerPlant example was a
fantastic eye-opener for me, because the 2 frameworks were very similar
in scope and functionality (even many of the classes were analagous) yet
the MI design was so much cleaner. But since then I see the same effect
in libraries that are not so similar: with SI designs I find myself
hunting up and down an overly deep hierarchy for functionality, always
being a little unclear on exactly why things are where they are, and
always having difficulty figuring out where to attach to the hierarchy
to extend it. With MI designs, I understand why things are where they
are, and know where to put my additions.
Smalltalkers often point out that learning the language is nearly
trivial, but learning the class library is quite difficult and time
consuming. They point to the depth and maturity of the libraries as the
cause (implying that the effort is more than worth it). But they are
only partly right; the difficulty of learning the libraries is GREATLY
compounded by the SI design. It is a large library and thus would
require significant effort to learn regardless, but it is also much
harder to learn than it would be if expressed as an MI design. (Yeah,
yeah, I know you don't just take an SI library and factor out a few
mixins. The SI/MI decision affects the library in many ways. What I mean
by "if expressed as an MI design" is if the same functionality were
provided in an MI library.) So while the effort is probably indeed more
than worth it, wouldn't it still be worthwhile to greatly reduce the
effort?
Hmm. Now let's talk about Java. Java recognizes, sort of, the problems
with SI but provides an extremely half-baked solution. Java gives you
protocols, so that you can if you wish structure your library in a very
MI style. But since protocols only provide an interface and do not
provide any implementation at all, Java does not provide reuse like MI.
You can (if you wish) get the nice inheritance graph that is more easily
comprehensible, but each class that "inherits" a protocol must implement
it, using one of two (not quite equally) bad techniques:
1) Reimplement the protocol everywhere, have tons of duplicated code to
maintain.
2) Slightly better, implement common logic for the protocol in a single
helper class and embed that class in all classes implementing the
protocol, and write methods IN EVERY CLASS IMPLEMENTING THE PROTOCOL
that just dispatch to the helper class. At least the code that is
duplicated is just simple and likely error-free dispatches. But changes
to the protocol still require changes to all classes that implement it!
Granted, some protocols require custom code implemented in each class
that implements it. But there is often common logic that could be
encapsulated into a class for reuse. Take for example comparing items
for ordering (in Eiffel the COMPARABLE class). The protocol could:
- specify operators for (in C-speak): == != < > <= >=
- require that each implementing class implement == and <
- provide default implementations for all other operators based on ==
and <
- allow any implementor to override those implementations for
efficiency.
Except you can't do this readily in Java. Either the protocol only
provides == and < and thus objects implementing it provide rather meager
comparison semantics, or the protocol provides all the operators and
every implementor must either implement them all or implement two and
dispatch the rest to a class that implements the rest in terms of those
two. Of course these functions are usually not hard to implement, but
any time you have to implement something 100 times instead of 1 you
incur an unhealthly risk of bugs. And don't even talk to me about how
utterly ridiculous it is to add to 100 classes 4 methods that do nothing
but dispatch to a helper class!!!
Now it probably is a good idea to separate type substitutability from
implementation reuse. But Java doesn't really separate them, it just
changes one piece in such a way as to confuse the issue even more than
the "classic" OO paradigm where inheritance specifies both. Ugly, ugly,
ugly!
(Objective-C at least has the decency to make delegation easier...)
Side note: I'm having difficulty finding my around the Dylan libraries
because the methods are not contained in the classes! But I don't think
this is an inherent flaw in the way Dylan libraries are structured. I
think it's just a temporary thing while I get used to a paradigm that is
different than the one that I'm used to. I've used different OO
languages and libraries, but for more than a decade (closer to 2) I've
always thought of methods as being part of classes. My problem with SI
libraries is not a problem with expectations or perception; it is a
problem with the overly-complex structure of those libraries.
Follow-Ups:
References: