One of the most controversial choices by the Kotlin designers was to make classes final by default.
This means we need to explicitly open
them to allow subclassing. Moreover, functions on an open class must also
be explicitly marked open
to allow a subclass to override them.
There were a lot of debate even before Kotlin 1.0 came out, as illustrated by this thread in the Kotlin forum. The main objections against defaulting to closed classes are the following:
- It prevents users of a library to extend code in ways not originally envisioned by the author;
- It prevents users of a library to fix bugs in the library code by overriding the faulty code;
- It prevents AOP frameworks to generate proxies by subclassing the original classes.
The last point was deemed critical enough that JetBrains decided to provide the “all-open” compiler plugin which “makes classes annotated with a specific annotation and their members open without the explicit open keyword".
Finally, despite the criticism, the decision to default to closed classes was taken and there is no coming back. What I would like to discuss in this article are the pitfalls of class inheritance (which were likely the reason the Kotlin designers took that decision) and then present one Kotlin language feature that provides a nice alternative.
Inheritance pitfalls
Let's turn to an authority on the matter. Joshua Bloch in his book Effective Java (3rd Edition) states (item 19):
Design and document for inheritance or else prohibit it
As he explains, the main culprit is self-use of overridable methods. What does it mean? Let's look at an example:
|
|
In the Foo
class, the barTwice()
function is implemented by calling its sibling function, bar()
, which is
overridable by a subclass.
To illustrate the kind of problems this can bring, we are going to extend Foo
in order to decorate all values
returned by its bar*()
functions by adding the String Ext
.
|
|
Nothing could be more straightforward, right? Let's test it out by calling the printExt()
function we added to
FooSub
:
|
|
I don't know about you, but this is not really what I expected…
The only way to get the value BarBarExt
from barTwice()
, is to re-implement it by calling
the original bar()
function from the superclass:
|
|
However reimplementing methods is inconvenient, error-prone and not always possible (in case the original function is accessing private properties not accessible from the subclass).
As Joshua Bloch summarizes:
In summary, designing a class for inheritance is hard work. You must document all of its self-use patterns, and once you’ve documented them, you must commit to them for the life of the class. If you fail to do this, subclasses may become dependent on implementation details of the superclass and may break if the implementation of the superclass changes. To allow others to write efficient subclasses, you may also have to export one or more protected methods. Unless you know there is a real need for subclasses, you are probably better off prohibiting inheritance by declaring your class final or ensuring that there are no accessible constructors.
Inheritance is hard to get right
Inheritance is not bad per se, but it's often overused and abused (and I'm guilty of this as anyone). It's a
powerful feature but should be limited to the use cases where it really applies, i.e. strong and stable is-a
relationships, where the subclass is a specialized subtype of the superclass, complying with the
Liskov Substitution Principle.
This is hard to get right. The short article
“Is a Square a Rectangle” is explains it well.
Even though, geometrically, a Square indeed is a specialized Rectangle, as soon as you add behaviors the relationship
breaks down. If Square is a subclass of Rectangle then you should be able to substitute any Rectangle with a Square and
preserve the correctness of the program. However it's not possible to override any Rectangle update operation in the
Square class without breaking Rectangle invariants. Take a simple Rectangle function add(deltaHeight, deltaWidth)
:
how can you implement it meaningfully in the Square subclass, which need to preserve its squareness? Breaking the
superclass invariants can lead to hard to catch bugs. The article concludes:
The Liskov Substitution Principle tells us that inheritance relationships should be based upon the external behaviour of types and not on the characteristics of the real-world objects that they may represent.
Composition with class delegation
What are the main reasons we often resort to inheritance, anyway?
- Modelling: represent the concepts of our domain
- Reuse: use the behaviours we need without rewriting them
- Customisation: modify the behavior of a class by overriding existing behaviours
- Extension: add new behaviours
Can we achieve all of this without inheritance?
In his book, Joshua Bloch also advise, in item 18, to favor composition over inheritance for a more flexible and robust design. Fortunately, the Kotlin designers read that chapter too and added to the language native support for composition, making it very natural and concise. Meet Kotlin class delegation.
To see composition with class delegation in action, let's first re-implement the previous example with the approach Joshua Bloch suggests in the book.
First, we need to extract an interface:
|
|
An implementation of Foo
, as in the example above, but with a closed class:
|
|
Joshua Bloch recommends to write a forwarding class, which role is to implement the interface by forwarding all function calls to an instance of the same interface. The idea here is to write all this boilerplate only once and reuse it every time we need to use composition.
|
|
Finally, we can implement our decoration using composition:
|
|
By using composition we are able to extend the behaviour of FooImpl
without subclassing it. However the suggested
approach has too much boilerplate.
With class delegation, Kotlin basically removes the need to manually write forwarding functions as the compiler
generates them for us. We can therefore get rid of the ForwardingFoo
class by simply declaring that the Foo
interface implementation need to be delegated to the foo
instance, unless we explicitly provide our own
overrides. Let's rewrite the FooWrapper
class:
|
|
By using composition with class delegation we can get the same benefits than with class inheritance:
- Modelling: same but based on interfaces instead of classes to model the domain;
- Reuse: the implementation from
FooImpl
is reused, without the fragility due to inheritance; - Customisation: we can provide our own implementation in
FooWrapper
when needed; - Extension: we can obviously add new behaviours by adding new member functions to
FooWrapper
.
As always, there are trade-offs. The following limitations apply:
- Only interfaces can be delegated to, i.e. the
by
clause cannot be applied to classes; - The wrapped instance can only access its own implementation of the interface, i.e. it cannot call back into the wrapper. This is somewhat the point because as discussed above self-use of overridable functions is problematic but in some cases this could be perceived as a limitation.
- Composition cannot be used if the wrapped instance pass a reference to itself (
this
) to other objects which then call back to it, because the wrapper will be ignored.
Conclusion
Coming from Java, Kotlin final classes may seem impractical (and this can be true occasionally). However, we observed that class inheritance has pitfalls and should be used with caution. Kotlin encourages disciplined use of class inheritance by forcing developers to enable it deliberately. At the same time Kotlin provides native support for composition with class delegation, which is a good alternative to inheritance.
All in all, the negative impact of this controversial decision is quite limited in practice and may actually encourage more robust code.