Inheritance Risks
This article will talk about the risks associated with class inheritance. Here we will show an alternative to class inheritance – composition. After reading, you will understand why Kotlin makes all classes final by default. The article will explain why you should not make a Kotlin class open (open), unless there are good reasons for that.
Let’s assume we have the following interface:
interface Insertable<T> {
fun insert(item: T)
fun insertAll(vararg items: T)
val items: List<T>
}
Interface Insertable (insertable)
Also, BaseInsert is an implementation of the interface Insertable<Number>
. BaseInsert
open
(open). Therefore, we can expand it.
Let CountingInsert
will be an extension BaseInsert
. Every time the code inserts Number
, it must increment the count variable by one. So we get:
class CountingInsert : BaseInsert() {
var count: Int = 0
private set
override fun insert(item: Number) {
super.insert(item)
count++
}
override fun insertAll(vararg items: Number) {
super.insertAll(*items)
count += items.size
}
}
Counting algorithm implemented through inheritance
This implementation should work. Line 8 increments count
per unit; line 13 – by the number of variable arguments.
The code does not work as expected. See line 7 below.
fun main(args: Array<String>) {
CountingInsert().apply {
insert(1)
insert(2)
insertAll(3, 4)
println(count) // prints 6, the incorrect value; should be 4
println(items) // prints [1 2 3 4]
}
}
CountingInsert()
gives wrong result
The error is on line 10 below:
open class BaseInsert : Insertable<Number> {
private val numberList = mutableListOf<Number>()
override fun insert(item: Number) {
numberList.add(item)
}
override fun insertAll(vararg items: Number) {
items.forEach { number -> insert(number) }
}
override val items: List<Number>
get() = numberList
}
BaseInsert Implementation
BaseInsert.insertAll
is a convenience function. Function insertAll
causes insert
for each element in the list vararg
. Class CountingInsert
recalculated the insertion of the numbers 3 and 4. CountingInsert.insertAll
executed statement twice count++
; once – operator count += items.size
. Function CountingInsert.insertAll
increased count
four instead of two.
Are there alternatives? Yes. We can change the code or use composition.
Changing the code seems like the obvious solution. Suppose we are allowed to change the base class. You can change the implementation BaseInsert.insertAll
on:
override fun insertAll(vararg items: Number) {
numberList += items.toList()
}
Updated implementation BaseInsert.insertAll
This implementation avoids calling BaseInsert.insert()
the source of our problems.
Let’s assume we don’t have access to the class BaseInsert
. Then you can remove the override insertAll()
:
class CountingInsert : BaseInsert() {
var count: Int = 0
private set
override fun insert(item: Number) {
super.insert(item)
count++
}
}
Class CountingInsert
without redefinition insertAll
Solving the problem by changing the code is quite vulnerable. Class CountingInsert
depends on the subtleties of implementation BaseInsert
. Is there a more efficient way? Yes, let’s use composition.
Here is an implementation using composition:
class CompositionInsert(private val insertable: Insertable<Number> = BaseInsert())
: Insertable<Number> by insertable {
var count: Int = 0
private set
override fun insert(item: Number) {
insertable.insert(item)
count++
}
override fun insertAll(vararg items: Number) {
insertable.insertAll(*items)
count += items.size
}
}
Implementation with composition
Suppose the class BaseInsert
uses the implementation from Fig. 4. After class testing InsertDelegation
the result will be correct. See line 15:
fun main(args: Array<String>) {
CountingInsert().apply {
insert(1)
insert(2)
insertAll(3, 4)
println(count) // prints 6, the incorrect value; should be 4
println(items) // prints [1 2 3 4]
}
CompositionInsert().apply {
insert(1)
insert(2)
insertAll(3, 4)
println(count) // prints 4, which is correct
println(items) // prints [1 2 3 4]
}
}
Test results for CompositionInsert
Comparing code snippets 2 and 7, we can say that the implementations insert
and insertAll
similar. See below:
// By inheritance
override fun insert(item: Number) {
super.insert(item)
count++
}
override fun insertAll(vararg items: Number) {
super.insertAll(*items)
count += items.size
}
// By delegation
override fun insert(item: Number) {
insertable.insert(item)
count++
}
override fun insertAll(vararg items: Number) {
insertable.insertAll(*items)
count += items.size
}
Comparing inheritance and composition
The methods being compared are the same with one exception. Inheritance uses super
and in the composition – insertable
. Compare lines 3 and 12; as well as 7 and 16 in Fig. 8. The delegation pattern delegates the execution of the insertion task to insertable
. Class CompositionInsert
increments a variable count
. Inheritance, on the other hand, breaks class encapsulation. BaseInsert
.
What is the root cause of the problem? Let’s pretend that BaseInsert
not open (open). See line 1 in code snippet 4. If BaseInsert
was final (final), then the Kotlin compiler would mark the code in fragments 2 and 5 as an error. Only the solution on fragment 7 would be workable. When we do class BaseInsert final
encapsulation BaseInsert
is not violated.
Kotlin understands the risks associated with inheritance. Kotlin forbids inheritance unless the developer marks the class as open
. Conclusion: In general, Kotlin classes should be final
unless there is a good reason to make the class open
.
We invite you to the open lesson “Basics of business logic and development of a library for the CoR template”. In this open lesson:
– let’s talk about the general principles of building the business logic of the application,
– consider frameworks for developing business logic,
– learn about design patterns such as facade and chain of responsibilities,
– we will develop a library for the “Chain of Responsibility” pattern using DSL and coroutines.
Registration for the class is open on the course page “Kotlin Backend Developer. Professional”.