When modeling a domain, you often want to express more than one "kind-of" relationship for an object. For example:
Array
is both indexed (i.e., you can perform
key/value lookups, like a hashtable) and ordered (i.e., the
elements have a sequence, like a linked list). You might want
it to inherit from both IndexedCollection
and
OrderedCollection
.ReadWriteStream
(representing a readable and
writeable file) is both a ReadStream
and a
WriteStream
.It would be nice to express these relationships directly in your design by having a class inherit from more than one superclass.
As in a single-inheritance context, a class inherits all of its superlasses' methods and instance variables.
However, since there is more than one superclass, problems with ambiguity arise:
There are several solutions to these problems...
Prohibit multiple inheritance with overlapping methods.
This is the most draconian solution. In practice, it doesn't work out too well --- there are too many opportunities that one must forego, particularly with diamond inheritance. Consider a class hierarchy:
class Shape { Float area() { ... } } class Rectangle sublasses Shape { Float area() { ... } } class Rhombus subclasses Shape { Float area() { ... } } class Square subclasses Rectangle, Rhombus {}
This is "diamond inheritance", so called because the class hierarchy has the shape shown in Fig. 1. Many "natural" inheritance hierarchies have this form --- some base class has two kinds of extensions, and one would like to combine them into a third kind that has the properties of both.
Clearly, to support diamond inheritance, one must be able to inherit from classes that define the same methods. Diamond inheritance is not the only case for supporting such multiple inheritance, but it is one of the most compelling.
Therefore, we require a solution that somehow allows the user to resolve the ambiguity among multiply inherited methods.
User resolves ambiguity by specifying textual ordering.
One simple way to resolve the ambiguity is to declare that the subclasses are not a set --- rather, they are an ordered list, with some superclasses taking priority over others. The Common Lisp Object System takes this approach: superclasses are searched from left-to-right (in order of textual declaration at the class definition) for methods. The first one found is the one executed.
In the above example, Rectangle
's
area()
method would take priority, because
Rectangle
appears leftmost in the declaration of
Square
.
This solution lacks flexibility --- what if you wanted to
inherit some methods from Rectangle
, and other
methods from Rhombus
? --- but it solves the
language semantics problem.
User resolves ambiguity by overriding in subclass and directing resends to one class.
In this solution, the programmer must override ambiguous methods and provide the preferred implementation. C++ and Extended Smalltalk take this approach.
In the example above, the programmer would be forced
to provide an area()
method in Square
.
This method would not be ambiguous, since it overrides both
Rectangle
and Rhombus
's versions.
Now, by itself, this solution is not satisfying, because now
it is no longer possible to reuse the code of multiply
inherited methods. Therefore, in order to enable code reuse,
the language should provide a special form of send called a
directed resend. A directed resend is a
generalization of a super
send: it allows the
programmer to send a message to self while specifying where
to begin method lookup.
For example, if the programmer wanted to reuse
Rhombus
's area
method in
Square
, the programmer might write (just to invent
a syntax):
class Square subclasses Rectangle, Rhombus { Float area() { return super(Rhombus).area(); // XXX } }
where the code on line XXX means "send myself the area() message, but begin lookup in my Rhombus superclass".
This solution has at least two clear advantages:
Recall our shape hierarchy. Suppose there were some instance
variables in Rectangle
and Rhombus
:
class Shape { Float area() { ... } } class Rectangle sublasses Shape { Float area() { ... } Point topLeft; Point bottomRight; } class Rhombus subclasses Shape { Float area() { ... } Point topLeft; Point topRight; Point bottomRight; } class Square subclasses Rectangle, Rhombus {}
Should Square
inherit one or two copies of
topLeft
and bottomRight
? This is the
duplicate instance variable problem.
With duplicate instance variables, a language designers have taken several approaches:
Merge duplicates. In the case of the shape hierarchy above, this answer seems "obviously right" --- a square only has one top left coordinate, and one bottom right coordinate, so we should just inherit one copy. This is what Common Lisp does in all cases: all inherited instance variables of some name are merged together, regardless of whether they were declared that way.
In other cases, however, the answer is not so obvious. Consider the following: hierarchy:*
class Window { List menuItems; } class Restaurant { List menuItems; } class RestaurantWindow subclasses Window, Restaurant {}
* Leave aside, for the moment, the fact
that this is arguably a bad object-oriented design, because a
RestaurantWindow
probably is in a has-a
relationship with Restuarant
, rather than a
is-a-kind-of relationship.
It seems likely that the menuItems
field of
Window
serves a different purpose than the
menuItems
field of Restaurant
: the
former probably contains entries for manipulating windows in a
GUI, whereas the latter probably contains dishes that the
restaurant serves.
In such cases, it is inappropriate to merge the items. Therefore, always merging is inappropraite.
Always duplicate. This is the converse
of the previous policy. We could always duplicate the inherited
fields, which would work fine for RestaurantWindow
but is clearly wrong for Square
.
Therefore, we must provide some strategy for the user to specify when to duplicate inherited fields of the same name...
Merge if originally from same declaration. With this strategy, the instance variables of a superclass are merged if they originate from the same textual declaration in the source program. Returning to our examples ---
menuItems
in the example
above originate from separate declarations (one in
Window
and one in Restaurant
),
they would not be merged. This is "what we want".topLeft
fields in
Rhombus
and Rectangle
originate in
two separate declarations, they would not be merged. This
is not what we want! However, we could get the
effect that we want by changing our hierarchy to the
following:
class Quadrilateral subclasses Shape { Point topLeft; Point bottomRight; } class Rectangle subclasses Quadrilateral { ... } class Rhombus subclasses Quadrilateral { Point topRight; } class Square subclasses Rectangle, Rhombus {}Now, since
topLeft
and bottomRight
both originate in the same textual declaration, the fields
would be merged in Square
.C++ uses a hybrid of the above approaches. In C++, inheritance links come in two kinds: regular, and virtual. For regular inheritance links, C++ uses "always duplicate"; for virtually inherited base classes, C++ uses the "merge if originally from same declaration" policy.
I am not familiar with any languages that use purely the "merge if originally from same declaration" policy.
Actually, none of the above approaches to field duplication
seems entirely satisfactory. Even the "merge if originally from
same declaration" requires the programmer to plan ahead and factor
classes in a particular way --- for example, the user had to factor
out the Quadrilateral
class ahead of time. It would
be better if the implementor of Square
had some way of
saying which features of superclasses it wanted to merge, and which
it wanted to duplicate.
Truthfully, the problem of doing multiple inheritance "right" is still an open problem in language design. However, relatively little work is being done on this subject directly, because the question of multiple-inheritance duplication policies is a hairy language-lawyerly detail whose correct answer probably depends on the empirical questions of what programmers need in large, real-world code bases.
On the other hand, some language designers would claim that if a problem has no "obviously right" answer, then the problem needs to be reframed. There is considerable research these days on novel behavior-composition mechanisms that go beyond inheritance. Perhaps one of these mechanisms would yield an "obviously right" answer that inheritance alone does not make obvious.
Recall that we said that inheritance and subtyping are distinct --- inheritance concerns implementations, and subtyping concerns interfaces.
Java prohibits multiple inheritance of implementation.
However, you will sometimes hear that Java supports "multiple
inheritance of interface". Of course, given our terminology, this
is an oxymoron, but what Java does support is multiple
by-name subtyping --- a class can implement
multiple interfaces, but it can only extend
one
superclass.
It turns out that, in practice, you can get much of the flexibility of multiple inheritance by using multiple subtyping.
Returning to our shape hierarchy, one could use Java interfaces and implementations:
interface IShape { Float area(); } interface IRectangle extends IShape { ... } interface IRhombus extends IShape { ... } interface ISquare extends IRhombus, IRectangle { ... } abstract class Shape implements IShape {} class Rectangle extends Shape implements IRectangle { Float area() { ... } } class Rhombus extends Shape implements IRectangle { Float area() { ... } } class Square extends Rhombus implements ISquare { }
Notice that Square
still can't inherit and reuse
code from both Rectangle
and
Rhombus
.
However, the ISquare
interface subtypes both the
IRectangle
interface and the IRhombus
interface. Therefore, provided our variables are declared to use
only the interface types and not the class types, we do
get to use a ISquare
wherever a
IRectangle
would be allowed, and wherever a
IRhombus
would be allowed.