Another look in Inheritance

2 minute read

We touched a bit on Inheritance concept. Inheritance is sometimes used to add functions to a class as a way of reuse for a new purpose.

But is this always a good choice?

Class delegation is midway between composition and inheritance. Like composition, we have to place the member inside our class. Like inheritance, it exposes the interface of the sub object.

open class Heater {
    fun heat(temp: Int) = "Heating to $temp"
}

class Aircon: Heater() {
    fun cool(temp: Int) = "Cooling to $temp"
}

fun warmingUp(heater: Heater) {
    heater.heat(70)
}

fun upAndDown(aircon: Aircon) {
    aircon.heat(70)
    aircon.cool(20)
}

val heater = Heater()
val aircon = Aircon()
warmingUp(heater)
warmingUp(aircon)
upAndDown(aircon)

The code above is seen logical. Since Heater cannot do all the functions we want, we need to create Aircon who inherits Heater and then add some extra cooling functions. However, you might recall in Upcasting that when you upcast, you lose some info.

Liskov Substitution Principle says functions that accept a base class must be able to use the objects of derived classes without knowing it. It is all substitutability.

warmingUp() takes heater as argument and also accept aircon due to Liskov Substitution Principle. You might lose some useful info about aircon during upcasting. Although modern OO programming allows the addition of functions during inheritance, this can be a “code smell”. It might negatively impact a later maintainer of the code as technical debt.

Alternative

What we really wanted in previous example is a Heater class with cool() function, so that upAndDown function works. Why not extension function? It does the same thing without inheritance.

fun Heater.cool(temp: Int) = "Cooling to $temp"

fun upAndDown(heater: Heater) {
    aircon.heat(70)
    aircon.cool(20)
}

Interface by Convention

An extension function can be considered as creating an interface containing a single function.

class X
fun X.f() {}

class Y
fun Y.f() {}

callF(x:X) = x.f()
callF(y:Y) = y.f()

Although both X and Y has a member function f(), but we don’t get polymorphic behaviour. callF() have to make separately for X and Y. The “interface by convention” is extensively used in Kotlin libraries, especially when dealing with collections.

Members vs Extension

There are cases where you are forced to use member functions rather than extensions. If a function must access a private member, you have no choice but to make it a member function:

class Z(var i: Int = 0) {
    private var j = 0
    fun inc() {
        i++
        j++
    }
}

fun Z.dec() {
    i--
    //j--
}

the variable j is not accessible since it is a private member of the class. The main drawback of the extension function is that they cannot be overridden.

Summary

Languages like C++ and Java allow inheritance unless you specially disallow it. Kotlin assumes that you won’t be using inheritance – it actively prevents inheritance and polymorphism unless they are intentionally allowed using the open keyword.

Often, functions are all you need. Sometimes objects are very useful. Objects are one tool among many, but they’re not for everything.

Consider whether you need inheritance at all, and apply the maxim Prefer extension functions and composition to inheritance.