# Inheritance & Extensions
Inheritance is sometimes used to add functions to a class as a way to reuse it for a new purpose. This can lead to code that is difficult to understand and maintain.
Suppose someone has created a Heater
class along with functions that act upon a Heater
:
// InheritanceExtensions/Heater.kt
package inheritanceextensions
import atomictest.eq
open class Heater {
fun heat(temperature: Int) =
"heating to $temperature"
}
fun warm(heater: Heater) {
heater.heat(70) eq "heating to 70"
}
For the sake of argument, imagine that Heater
is far more complex than this, and that there are many adjunct functions such as warm()
. We don’t want to modify this library—we want to reuse it as-is.
If what we actually want is an HVAC
(Heating, Ventilation and Air Conditioning) system, we can inherit Heater
and add a cool()
function. The existing warm()
function, and all other functions that act upon a Heater
, still work with our new HVAC
type—which would not be true if we had used composition:
// InheritanceExtensions/InheritAdd.kt
package inheritanceextensions
import atomictest.eq
class HVAC : Heater() {
fun cool(temperature: Int) =
"cooling to $temperature"
}
fun warmAndCool(hvac: HVAC) {
hvac.heat(70) eq "heating to 70"
hvac.cool(60) eq "cooling to 60"
}
fun main() {
val heater = Heater()
val hvac = HVAC()
warm(heater)
warm(hvac)
warmAndCool(hvac)
}
This seems practical: Heater
didn’t do everything we wanted, so we inherited HVAC
from Heater
and tacked on another function.
As you saw in [Upcasting](javascript:void(0)), object-oriented languages have a mechanism to deal with member functions added during inheritance: the added functions are trimmed off during upcasting and are unavailable to the base class. This is the Liskov Substitution Principle, aka “Substitutability,” which says functions that accept a base class must be able to use objects of derived classes without knowing it. Substitutability is why warm()
still works on an HVAC
.
Although modern OO programming allows the addition of functions during inheritance, this can be a “code smell”—it appears to be reasonable and expedient but can lead you into trouble. Just because it seems to work doesn’t mean it’s a good idea. In particular, it might negatively impact a later maintainer of the code (which might be you). This kind of problem is called technical debt.
Adding functions during inheritance can be useful when the new class is rigorously treated as a base class throughout your system, ignoring the fact that it has its own bases. In [Type Checking](javascript:void(0)) you’ll see more examples where adding functions during inheritance can be a viable technique.
What we really wanted when creating the HVAC
class was a Heater
class with an added cool()
function so it works with warmAndCool()
. This is exactly what an extension function does, without inheritance:
// InheritanceExtensions/ExtensionFuncs.kt
package inheritanceextensions2
import inheritanceextensions.Heater
import atomictest.eq
fun Heater.cool(temperature: Int) =
"cooling to $temperature"
fun warmAndCool(heater: Heater) {
heater.heat(70) eq "heating to 70"
heater.cool(60) eq "cooling to 60"
}
fun main() {
val heater = Heater()
warmAndCool(heater)
}
Instead of inheriting to extend the base class interface, extension functions extend the base class interface directly, without inheritance.
If we had control over the Heater
library, we could design it differently, to be more flexible:
// InheritanceExtensions/TemperatureDelta.kt
package inheritanceextensions
import atomictest.*
class TemperatureDelta(
val current: Double,
val target: Double
)
fun TemperatureDelta.heat() {
if (current < target)
trace("heating to $target")
}
fun TemperatureDelta.cool() {
if (current > target)
trace("cooling to $target")
}
fun adjust(deltaT: TemperatureDelta) {
deltaT.heat()
deltaT.cool()
}
fun main() {
adjust(TemperatureDelta(60.0, 70.0))
adjust(TemperatureDelta(80.0, 60.0))
trace eq """
heating to 70.0
cooling to 60.0
"""
}
In this approach, we control the temperature by choosing among multiple strategies. We could also have made heat()
and cool()
member functions instead of extension functions.
# Interface by Convention
An extension function can be thought of as creating an interface containing a single function:
// InheritanceExtensions/Convention.kt
package inheritanceextensions
class X
fun X.f() {}
class Y
fun Y.f() {}
fun callF(x: X) = x.f()
fun callF(y: Y) = y.f()
fun main() {
val x = X()
val y = Y()
x.f()
y.f()
callF(x)
callF(y)
}
Both X
and Y
now appear to have a member function called f()
, but we don’t get polymorphic behavior so we must overload callF()
to make it work for both types.
This “interface by convention” is used extensively in the Kotlin libraries, especially when dealing with collections. Although these are predominantly Java collections, the Kotlin library turns them into functional-style collections by adding a large number of extension functions. For example, on virtually any collection-like object, you can expect to find map()
and reduce()
, among many others. Because the programmer comes to expect this convention, it makes programming easier.
The Kotlin standard library Sequence
interface contains a single member function. The other Sequence
functions are all extensions (opens new window)—there are well over one hundred. Initially, this approach was used for compatibility with Java collections, but now it’s part of the Kotlin philosophy: Create a simple interface containing only the methods that define its essence, then create all auxiliary operations as extensions.
# The Adapter Pattern
A library often defines a type and provides functions that accept parameters of that type and/or return that type:
// InheritanceExtensions/UsefulLibrary.kt
package usefullibrary
interface LibType {
fun f1()
fun f2()
}
fun utility1(lt: LibType) {
lt.f1()
lt.f2()
}
fun utility2(lt: LibType) {
lt.f2()
lt.f1()
}
To use this library, you must somehow convert your existing class to LibType
. Here, we inherit from an existing MyClass
to produce MyClassAdaptedForLib
, which implements LibType
and can thus be passed to the functions in UsefulLibrary.kt
:
// InheritanceExtensions/Adapter.kt
package inheritanceextensions
import usefullibrary.*
import atomictest.*
open class MyClass {
fun g() = trace("g()")
fun h() = trace("h()")
}
fun useMyClass(mc: MyClass) {
mc.g()
mc.h()
}
class MyClassAdaptedForLib :
MyClass(), LibType {
override fun f1() = h()
override fun f2() = g()
}
fun main() {
val mc = MyClassAdaptedForLib()
utility1(mc)
utility2(mc)
useMyClass(mc)
trace eq "h() g() g() h() g() h()"
}
Although this does extend a class during inheritance, the new member functions are used only for the purpose of adapting to UsefulLibrary
. Note that everywhere else, objects of MyClassAdaptedForLib
can be treated as MyClass
objects, as in the call to useMyClass()
. There’s no code that uses the extended MyClassAdaptedForLib
where users of the base class must know about the derived class.
Adapter.kt
relies on MyClass
being open
for inheritance. What if you don’t control MyClass
and it’s not open
? Fortunately, adapters can also be built using composition. Here, we add a MyClass
field inside MyClassAdaptedForLib
:
// InheritanceExtensions/ComposeAdapter.kt
package inheritanceextensions2
import usefullibrary.*
import atomictest.*
class MyClass { // Not open
fun g() = trace("g()")
fun h() = trace("h()")
}
fun useMyClass(mc: MyClass) {
mc.g()
mc.h()
}
class MyClassAdaptedForLib : LibType {
val field = MyClass()
override fun f1() = field.h()
override fun f2() = field.g()
}
fun main() {
val mc = MyClassAdaptedForLib()
utility1(mc)
utility2(mc)
useMyClass(mc.field)
trace eq "h() g() g() h() g() h()"
}
This is not quite as clean as Adapter.kt
—you must explicitly access the MyClass
object as seen in the call to useMyClass(mc.field)
. But it still handily solves the problem of adapting to a library.
Extension functions seem like they might be very useful for creating adapters. Unfortunately, you cannot implement an interface by collecting extension functions.
# Members versus Extensions
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:
// InheritanceExtensions/PrivateAccess.kt
package inheritanceextensions
import atomictest.eq
class Z(var i: Int = 0) {
private var j = 0
fun increment() {
i++
j++
}
}
fun Z.decrement() {
i--
// j -- // Cannot access
}
The member function increment()
can manipulate j
, but the extension function decrement()
doesn’t have access to j
because j
is private
.
The most significant limitation to extension functions is that they cannot be overridden:
// InheritanceExtensions/NoExtOverride.kt
package inheritanceextensions
import atomictest.*
open class Base {
open fun f() = "Base.f()"
}
class Derived : Base() {
override fun f() = "Derived.f()"
}
fun Base.g() = "Base.g()"
fun Derived.g() = "Derived.g()"
fun useBase(b: Base) {
trace("Received ${b::class.simpleName}")
trace(b.f())
trace(b.g())
}
fun main() {
useBase(Base())
useBase(Derived())
trace eq """
Received Base
Base.f()
Base.g()
Received Derived
Derived.f()
Base.g()
"""
}
The trace
output shows that polymorphism works with the member function f()
but not the extension function g()
.
When a function doesn’t need overriding and you have adequate access to the members of a class, you can define it as either a member function or an extension function—a stylistic choice that should maximize code clarity.
A member function reflects the essence of a type; you can’t imagine the type without that function. Extension functions indicate “auxiliary” or “convenience” operations that support or utilize the type, but are not necessarily essential to that type’s existence. Including auxiliary functions inside a type makes it harder to reason about, while defining some functions as extensions keeps the type clean and simple.
Consider a Device
interface. The model
and productionYear
properties are intrinsic to Device
because they describe key features. Functions like overpriced()
and outdated()
can be defined either as members of the interface or as extension functions. Here they are member functions:
// InheritanceExtensions/DeviceMembers.kt
package inheritanceextensions1
import atomictest.eq
interface Device {
val model: String
val productionYear: Int
fun overpriced() = model.startsWith("i")
fun outdated() = productionYear < 2050
}
class MyDevice(
override val model: String,
override val productionYear: Int
): Device
fun main() {
val gadget: Device =
MyDevice("my first phone", 2000)
gadget.outdated() eq true
gadget.overpriced() eq false
}
If we assume overpriced()
and outdated()
will not be overridden in subclasses, they can be defined as extensions:
// InheritanceExtensions/DeviceExtensions.kt
package inheritanceextensions2
import atomictest.eq
interface Device {
val model: String
val productionYear: Int
}
fun Device.overpriced() =
model.startsWith("i")
fun Device.outdated() =
productionYear < 2050
class MyDevice(
override val model: String,
override val productionYear: Int
): Device
fun main() {
val gadget: Device =
MyDevice("my first phone", 2000)
gadget.outdated() eq true
gadget.overpriced() eq false
}
Interfaces that only contain descriptive members are easier to comprehend and reason about, so the Device
interface in the second example is probably a better choice. Ultimately, however, it’s a design decision.
- -
Languages like C++ and Java allow inheritance unless you specifically 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. This provides insight into Kotlin’s orientation:
Often, functions are all you need. Sometimes objects are very useful. Objects are one tool among many, but they’re not for everything.
If you’re pondering how to use inheritance in a particular situation, consider whether you need inheritance at all, and apply the maxim Prefer extension functions and composition to inheritance (modified from the book Design Patterns (opens new window)).
Exercises and solutions can be found at www.AtomicKotlin.com.