The Perils of Inheritance
This article will show the perils of class inheritance. This article will illustrate the alternative to class inheritance — composition. After reading this article, you will understand why Kotlin makes all classes final by default. This article will explain why you should not make a Kotlin class open
unless there’s a good reason to do so.
Suppose that we have the following interface:
Further, BaseInsert
is an implementation of the Insertable<Number>
interface. BaseInsert
is open
. So, we can extend BaseInsert
.
We want CountingInsert
to extend BaseInsert
. Each time the code inserts a Number
, the code must increase the variablecount
by one. So, we have:
This implementation ought to work. Line 8 increases count
by one; line 13 by the number of variable arguments.
The code does not work as expected. See line 7 of Fig. 3 below.
The problem is at line 10 of Fig. 4 below:
The BaseInsert.insertAll
function is a convenience function. The insertAll
function calls insert
for each item in the vararg
list. The CountingInsert
class double counted the insertion of integers 3 and 4. CountingInsert.insertAll
executed the statement count++
twice; the statement count += items.size
once. The CountingInsert.insertAll
function increased count
by four instead of two.
Are there any alternatives? Yes. We can change the code, or we can use composition.
Changing the code seems to be an obvious solution. Suppose we are allowed to change the base class. We can change the implementation of BaseInsert.insertAll
to:
This implementation avoids calling BaseInsert.insert()
, the source of our trouble.
Suppose we don’t have access to the BaseInsert
class. Then we can remove the override to insertAll()
. See Fig. 6:
Resolving the problem through code changes is fragile. The CountingInsert
class relies upon the implementation details of BaseInsert
. Is there an even better way? Yes, let’s use composition.
Fig. 7 is the implementation by composition:
Assume that the BaseInsert
class uses the Fig. 4 implementation. When we test the InsertDelegation
class, the result is correct. See line 15 of Fig. 8 below.
Comparing Figs. 2 and 7, the implementations of insert
and insertAll
are similar. See Fig 9. below.
The comparable methods are the same with one exception. Inheritance uses super
; composition uses insertable
. Compare lines 3 versus 12; and 7 versus 16 of Fig. 8. The delegation pattern leaves the insertion task to the insertable
. The CompositionInsert
class increments the count
variable. By contrast, inheritance breaks the encapsulation of the BaseInsert
class.
What was the root cause of the problem? Suppose that BaseInsert
was not open
. See line 1 of Fig. 4. If BaseInsert
were final
, then the Kotlin compiler would flag the code in Figs. 2 and 5 as errors. Only the solution in Fig. 7 would be workable. When we make the BaseInsert
class final
, the encapsulation of BaseInsert
is not broken.
Kotlin understands the perils of inheritance. Kotlin forbids inheritance unless the developer marks the class as open
. Conclusion: in general, Kotlin classes should be final
unless there’s a good reason to make the class open
.
Do you need a Kotlin workshop? Visit our website to see what we can do for you.
To be up-to-date with great news on Kt. Academy, subscribe to the newsletter, observe Twitter and follow on medium.