Kotlin closed classes: how to compose with them

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:

1
2
3
4
5
6
open class Foo {
    open fun bar() = "Bar"
    open fun barTwice() = bar().repeat(2)
    open fun anotherFunction() = ...
    open fun andAnotherOne() = ...
}

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.

1
2
3
4
5
class FooSub: Foo() {
    override fun bar() = super.bar()  + "Ext"
    override fun barTwice() = super.barTwice() + "Ext"
    fun printExt() = println("${bar()} & ${barTwice()}" )
}

Nothing could be more straightforward, right? Let's test it out by calling the printExt() function we added to FooSub:

1
2
FooSub().printExt()
> BarExt & BarExtBarExtExt

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:

1
2
3
4
5
class FooSub: Foo() {
    override fun bar() = super.bar()  + "Ext"
    override fun barTwice() = super.bar().repeat(2) + "Ext"
    ...
}

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:

1
2
3
4
5
6
interface Foo {
  fun bar(): String
  fun barTwice(): String
  fun anotherFunction()
  fun andAnotherOne()
}

An implementation of Foo, as in the example above, but with a closed class:

1
2
3
4
5
6
class FooImpl: Foo {
  override fun bar() = "Bar"
  override fun barTwice() = bar().repeat(2)
  override fun anotherFunction() = ...
  override fun andAnotherOne() = ...
}

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.

1
2
3
4
5
6
open class ForwardingFoo(private val foo: Foo): Foo {
  override fun bar() = foo.bar()
  override fun barTwice() = foo.barTwice()
  override fun anotherFunction() = foo.anotherFunction()
  override fun andAnotherOne() = foo.andAnotherOne()
}

Finally, we can implement our decoration using composition:

1
2
3
4
5
6
7
8
class FooWrapper(private val foo: Foo) : ForwardingFoo(foo) {
  override fun bar() = foo.bar() + "Ext"
  override fun barTwice() = foo.barTwice() + "Ext"
  fun printExt() = println("${bar()} & ${barTwice()}")
}

FooWrapper(FooImpl()).printExt()
> BarExt & BarBarExt

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:

1
2
3
4
5
class FooWrapper(private val foo: Foo) : Foo by foo {
  override fun bar() = foo.bar() + "Ext"
  override fun barTwice() = foo.barTwice() + "Ext"
  fun printExt() = println("${bar()} & ${barTwice()}" )
}

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.

kotlin 

comments powered by Disqus