The
discussion about
multiple inheritance
has been going on for a long time.
I consider Java interfaces to be a clean and resilient solution.
It allows multiple inheritance for units that don't contain implementations,
thus no instance field duplication or method shadowing can occur.
Moreover interfaces are a design tool, i.e. you can specify software without implementing it!
Nevertheless, following the sign of times, Java 8 allows interfaces to also contain implementations,
but in a restricted way.
Other languages like C# and Scala always allowed multiple inheritance.
Scala provides traits,
comparable to Java's abstract classes,
and follows a "last wins" strategy concerning method shadowing.
Basically this discussion exists just because OO inheritance exists. Inheritance is the most elegant way to reuse code. Its sibling delegation is kind of code-duplication, and recommendable just where inheritance can't work, e.g. because a super-class already exists. That's the problem. We statically bind classes to super-classes. Then we would like to reuse them in another context, which is not possible due to that super-class. So what exactly would we like to do?
We would like to combine pieces of code freely with each other. We'd like to attach a Colorable and a Shadowable decoration to a PushButton component. Tomorrow maybe we also want it to be Borderable.
Aspect-oriented programming languages already exist, but they are currently just around other languages, jumping in for cross-cutting concerns. That's not the way we think normally, communicating and understanding mostly happens hierarchically, like in a document's table-of-contents.
Mixins
The ES6 world talks about "mixins". That's an OO term designating the classes involved in multiple inheritance. There are a lot of different ES6 multiple inheritance implementations around on the web, but none really works well. The official proposal works, but it requires mixin-classes to be written in a special way. That is what I want to look at in this Blog.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | const One = (SuperClass = Object) => class extends SuperClass { sayOne() { return "One" } } const Two = (SuperClass = Object) => class extends SuperClass { sayTwo() { return "Two" } } class OneTwo extends One (Two ()) /* extends One, Two */ { } const oneTwo = new OneTwo() console.log( "sayOne(): "+oneTwo.sayOne()+"\n"+ "sayTwo(): "+oneTwo.sayTwo() ) |
This is a minimal example for a class extending two others, or let's say mixing-in two others. Please refer to source code below for further explanations.
Mind that when using modules, you can write this differently, and give the mixin a non-duplicated class name:
export default (SuperClass = Object) => class One extends SuperClass { .... }
To be imported by:
import One from "./one.js" import Two from "./two.js" class OneTwo extends One (Two ()) { .... }
Examples
Click onto one of the buttons to get an example script into the text area.
Below the script you find an output area for console.log()
statements,
and on the very bottom there is an explanation of the example.
Resume
One
is a factory (mixin class factory)One()
returns a new class (mixin class)OneTwo
is a class extending mixin classes created by mixin factories at runtime
Some big questions remain open that really endanger practical usability of the mixin concept:
-
How can a mixin extend a certain other mixin it depends on?
For example, a
Shadowable
mixin requires aMouseObserver
mixin being present, how can we ensure that the final mixed class actually contains aMouseObserver
? - How can a mixin extend several other mixins it depends on? Currently just classes can mix them together. What we also need is to merge several mixins into a parent-mixin!
Multiple inheritance will stay under discussion. The central challenge here is to find a better way for code reuse than inheritance, which is clean only as single inheritance.
A minimal example for inheritance chaining with mixins.
A mixin class factory is defined by an expression like
const A = (SuperClass) => class extends SuperClass
.
The factory-name is A
(not the class name!).
The class-creation behind "=" happens through an "arrow-class", which is basically the same as an
"arrow-function".
I have put parentheses around it to make it clearer, although they are not needed.
(SuperClass)
is the parameter for the arrow-class.
Mind that SuperClass
is NOT a keyword, it is just a parameter name, could also be Base
.
Behind the arrow there is the class-signature, without a class name,
and here the SuperClass
parameter is used after class extends
to actually dynamically(!) extend the class given as parameter.
What follows is a normal ES6 class-body.
Result is a mixin factory of name A
that requires a super-class.
When you call that factory via A()
, you get back a concrete class.
It is the expression class AB extends A(B(Object))
that "mixes"
the super-classes A
and B
together into AB
.
Because for "mixable classes" it is not possible to receive no super-class,
we have to pass the Object
class to the definition of B(Object)
.
Try to remove that parameter. You will get "class heritage SuperClass is not an object or null" or similar.
The next example will show how to get around that pitfall.
Finally we instantiate the class AB
and call all its methods.
The console output should be visible above.
This example changes the way how a mixin factory is written.
First it removes the unneeded parentheses around the arrow-class.
Second it introduces a default parameter value (SuperClass = Object)
in case no super-class was given by a caller.
This adds a little waste to the mixin definition,
but relieves callers from the innermost Object
parameter (see "simpleSample").
This is the obligatory instanceof
test.
It should prove that the object ab
is of class A
, B
and AB
.
Testing against A
and B
showed
TypeError: 'prototype' property of A is not an object.
Just the test against class AB
worked correctly.
Explanation is that the mixable factories A
and B
need to be
called to deliver classes. Only A()
gives you a class, A
is just the factory!
Thus all super-classes have to be stored somewhere after retrieving them from their factory.
This is done in BClass
and AClass
.
They are the concrete super-classes of AB
.
Testing ab instanceof A()
yields false
,
because the factory creates a new class on any call!
Mind that using instanceof
is an
anti-pattern,
because implementations
should not speculate
with properties or behaviors of classes.
Constructors are present, including default values for their parameter.
The question rises which default value for firstName
("a"
or "b"
) will prevail in the mixed class AB
.
It turns out that the first wins, in this case class A
,
being the leftmost in the inheritance chain of class AB extends A(B())
.
Proven by output "sayA(): A firstName = a".
Mind that all mixin constructors must call super()
in some way.
This is because they always derive a given class, and ES6 requires the super()
call in that case.
Of course they can't know which parameters their dynamically given super-class will require,
it is the responsibility of the developer that nothing goes wrong here.
(In other words, also this kind of multiple inheritance works just under certain circumstances!)
Finally we change the value of the instance field firstName
to "xy"
.
ES6 class fields are always public and mutable (like it was in JavaScript).
The output shows that we have just one instance-field firstName
in class AB
,
and now its value is "xy"
.
This is about overriding in multiple inheritance, the critical topic.
For simplicity I removed all constructors, they can not be overridden.
Class A
overrides the sayB()
method of B
.
Also B
tries to override the sayA()
method of A
,
but doesn't succeed because it is the last in inheritance chain class AB extends A (B ())
.
This bug would uncover just at runtime.
In case B.sayA()
was not overridden and it would call super.sayA()
,
a runtime error would be thrown.
That means overrides depend on the inheritance-chain order: the first wins,
the last is not allowed to make super-calls in overrides.
Again it is the responsibility of the developer to avoid the latter.
The expression this.constructor.name
gives the class-name.
All of the mixed-in classes A
and B
state AB
as their class name,
which is what we would expect.
What has to be proven is that also
symbol properties
make it into the sub-class.
That means, when a super-class retrieved from mixin factory A
has a symbol property,
that property must be available also in sub-class AB
.
Symbol-properties always have to be accessed using [ square brackets ].
They must be defined outside of the class.
Mind that also symbol-properties are public and writable,
the only way to hide them is to hide the symbol key,
but even this is not safe against Object.getOwnPropertySymbols(object)
.
Keine Kommentare:
Kommentar veröffentlichen