Inheritance, composition, delegation, and traits

Marcin Moskala
Kt. Academy
Published in
10 min readJul 30, 2018

--

Sharing common behavior is one of the most important things in programming. By programming, we represent knowledge which changes over time. The more instances of the same knowledge, the harder it is to change it. Just to operate on a concrete example, let’s take a common behavior of RxJava subscriptions collecting and unsubscribing in onDestroy:

var subscriptions: List<Subscription> = listOf()fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}

We need to use it in all classes that implement some logic because they all produce subscriptions. We don’t know it yet, but soon RxJava 2 will come to our project, and we will need to change it into the following pattern:

val subscriptions = CompositeDisposable()fun onDestroy() {
subscriptions.dispose()
}
operator fun CompositeDisposable.plus(d: Disposable) {
subscriptions.add(d)
}

This explains why copy-pasting previous pattern into every single class we use would be a very bad idea. Changing this in all classes would be very hard. So good, we were smart and we remembered about code reuse ;) Let’s discuss our alternatives today.

Problems of inheritance

Classes are fashionable and look good, but they are not really liked by programmers anymore.

Inheritance is the most intuitive way to reuse code in OOP (object-oriented programming). We might use it to solve this problem and define our pattern inside an open class:

open class RxJavaUser {
var subscriptions: List<Subscription> = listOf()
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}

Although inheritance has a lot of downsides. For instance, we cannot extend more than a single class in JVM. As a result, we end up with Base classes for everything: BaseActivity, BasePresenter, BaseViewAdapter, … Each of them aggregate methods and properties that are often used in subclasses. Although it is not a good pattern. Mixing multiple functionalities in a single class is not good. We would also probably need to include our subscription and unsubscription capabilities in many base classes like BaseActivity, BaseFragment etc. What is more, it is also a bad practice to take capabilities you don’t need.

There are more problems with inheritance. The big one is that inheritance breaks encapsulation. Take a look at the following class:

class CountingHashSet<T> : HashSet<T>() {

var addCount = 0
private set

override fun
add(o: T): Boolean {
addCount += 1
return super.add(o)
}

override fun addAll(collection: Collection<T>): Boolean {
addCount += collection.size
return super
.addAll(collection)
}
}

Here is usage:

val countingSet = CountingHashSet()
countingSet.addAll(listOf(1,2,3))
print(countingSet.addCount)

What is the result?

No, it is not “3”. It prints “6”.

The reason is that addAll uses add method under the hood. Well, so maybe we can just delete addition in addAll? Yes we can, and we should probably do that — we and many other developers and library creators. Java creators know that and they know that they cannot change HashSet implementation anymore. It would just break all libraries that depend on its internal implementation, and it would cause unexpected and hard to find errors. This is why we should make our classes final, and why we should prefer alternatives over inheritance. The most common alternative is a composition.

Composition

Composition is a complex word that in practice means that one class contains an instance of another and uses its capabilities. For instance, subscriptions collecting and unsubscribing capabilities can be held in a separate class and used using composition:

class SubscriptionsCollector {
private var subscriptions: List<Subscription> = listOf()
fun add(s: Subscription) {
subscriptions += s
}
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter {
val subscriptionsCollector = SubscriptionsCollector()
fun onDestroy() {
subscriptionsCollector.onDestroy()
}
//...
}

Notice that our SubscriptionsCollector is the same concept as RxJava 2 CompositeDisposable. It means that we will have in the future additional wrapping. It is not bad though. The advantage is that we have control over this wrapping and if we would have a need to migrate to let’s say RxJava 3 then we would just replace usage in a single class.

Usage is not so easy though. We need to declare onDestroy every time and we need to use subscriptionsCollector every time we want to add another subscription. Inheritance gives simpler usage. It also gives polymorphic behavior. For instance, we might define CountingHashSet using composition in the following way:

class CountingHashSet<T> {
val set = HashSet<T>()
var addCount = 0
private set

fun
add(o: T): Boolean {
addCount += 1
return set.add(o)
}

fun addAll(collection: Collection<T>): Boolean {
addCount += collection.size
return set
.addAll(collection)
}
}

Although this class is not a MutableSet and it does not implement any other functions than add and addAll. We can make it implement MutableSet, but we would end up with the following monster:

class CountingHashSet<T>: MutableSet<T> {

val set = HashSet<T>()
var addCount = 0
private set

override fun
add(element: T): Boolean {
addCount += 1
return set.add(element)
}

override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return set
.addAll(elements)
}

override fun clear() {
set.clear()
}

override fun iterator(): MutableIterator<T> {
return set.iterator()
}

override fun remove(element: T): Boolean {
return set.remove(element)
}

override fun removeAll(elements: Collection<T>): Boolean {
return set.removeAll(elements)
}

override fun retainAll(elements: Collection<T>): Boolean {
return set.retainAll(elements)
}

override val size: Int
get() = set.size

override fun
contains(element: T): Boolean {
return set.contains(element)
}

override fun containsAll(elements: Collection<T>): Boolean {
return set.containsAll(elements)
}

override fun isEmpty(): Boolean {
return set.isEmpty()
}
}

This is an example of delegation — pattern that uses composition — and Kotlin supports it and allows simpler notation.

Delegation

The above huge class can be replaced with the following implementation:

class CountingHashSet<T>(
val innerSet: MutableSet<T> = HashSet<T>()
) : MutableSet<T> by innerSet {

var addCount = 0
private set

override fun
add(element: T): Boolean {
addCount += 1
return innerSet.add(element)
}

override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return innerSet
.addAll(elements)
}
}

It does exactly the same — methods from MutableSet that are not implemented will be generated with bodies that just use innerSet. As a result, we have polymorphic behavior (we implement MutableSet), code reuse and short declaration. This is perfect. Although this pattern is not so commonly used. What about our subscriptions management? Can we use it there? Well, we probably can:

interface SubscriptionsCollector {
var subscriptions: List<Subscription>
fun onDestroy()
}
class SubscriptionsCollectorImpl {
var subscriptions: List<Subscription> = listOf()
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter(
val subscrCollector = SubscriptionsCollectorImpl()
): SubscriptionsCollector by subscrCollector {
//...
}

This usage is terrifying and it is messing with our arguments. It would be also unintuitive to make custom onDestroy. The only profit is that we can add subscriptions in the same way as when we were using inheritance. I would definitely not use delegation support to solve this problem!

Traits

So many attempts and it is still hard to find a good alternative to solve our problem. Although, there is another technique that should be a part of every programmer’s toolkit.We can use traits. In Kotlin, interfaces are traits (historically even keyword trait was used instead of interface). In practice, it means that we can define default bodies for methods and declare properties (but without any actual values so they still have to be overridden in a non-abstract class):

interface NumberHolder {
val number: Int
fun doubled() = number * 2
}
class TenNumberHolder: NumberHolder {
override val number = 10
}
print(TenNumberHolder().doubled()) // 20

(Mixins is a similar concept, but in mixins, we can also hold state. For instance, we would be able to declare properties with default values. They are not yet supported by Kotlin.)

The concept of traits is very powerful. Let’s play with it for a while. Let’s say we write a fight simulator. Every fighter is expressed as a class. They all have some profession and together with this profession, they have some characteristics. There are also monsters which are not characters. We want to allow battle simulation between this characters.

Let’s start with fighting capabilities. We can express fighting capabilities using an interface:

interface Fighter {
var lifePoints: Int
fun attack(opponent: Fighter)
fun turnEnd() {}
}

Monsters are just fighters:

data class Goblin(override var lifePoints: Int = 50) : Fighter {

override fun attack(opponent: Fighter) {
println("Goblin attack (5)")
opponent.lifePoints -= 5
}
}

There are also characters. Let’s say that they additionally have names:

interface Character : Fighter {
val name: String
get() = this.javaClass.name
}

The behavior of the name construction base on a class name is already a trait. Although this is just a beginning. There are few classes of characters. Everyone can be Sorcerer and/or Warrior. Since we cannot extend more than a single class, we cannot use inheritance to express Warrior and Sorcerer capabilities. We will use traits instead. Let’s say that Warrior can use meleeAttack. Hit points depend on his strength, so we need to require such property as well:

interface Warrior : Character {
val strength: Int

fun meleeAttack(opponent: Fighter) {
println("$name melee attack with power $strength")
opponent.lifePoints -= strength
}
}

Sorcerer on the other hand, can castSpell. He needs to have some spell and mana points.

interface Sorcerer : Character {
val spell: Spell
var manaPoints: Int

fun canCastSpell() = manaPoints > spell.manaCost

fun
castSpell(opponent: Fighter) {
if (manaPoints < spell.manaCost) {
println("$name tried to cast spell but not enough mana")
return
}
println("$name cast spell ${spell.strength}")
manaPoints -= spell.manaCost
opponent.lifePoints -= spell.strength
}

override fun turnEnd() {
manaPoints += 1
}
}

Note that there is also declared a default way how mana points are restored (in turnEnd). Now we can declare some characters using the above professions. Let’s say we have Minsk, powerful warrior so he fights using meleeAttack:

data class Minsk(
override var lifePoints: Int = 60
) : Warrior {
override val strength: Int = 15

override fun attack(opponent: Fighter) {
meleeAttack(opponent)
}
}

We have also Arthur, who is both a warrior and a sorcerer. He fights using both spells and melee attacks:

data class Artur(
override var lifePoints: Int = 80,
override var manaPoints: Int = 10
) : Warrior, Sorcerer {
override var spell = Spell(4, 17)
override val strength: Int = 5

override fun attack(opponent: Fighter) {
if (canCastSpell()) {
castSpell(opponent)
} else {
meleeAttack(opponent)
}
}
}

We can simulate some fights between them:

fun simulateCombat(c1: Fighter, c2: Fighter) {
while (c1.lifePoints > 0 && c2.lifePoints > 0) {
c1.attack(c2)
c2.attack(c1)
c1.turnEnd()
c2.turnEnd()
}
val text = when {
c1.lifePoints > 0 -> "$c1 won"
c2.lifePoints > 0 -> "$c2 won"
else -> "Both $c1 and $c2 are dead"
}
println(text)
}
simulateCombat(Artur(), Minsk())
Artur cast spell 17
Minsk melee attack with power 15
Artur cast spell 17
Minsk melee attack with power 15
Artur melee attack with power 5
Minsk melee attack with power 15
Artur cast spell 17
Minsk melee attack with power 15
Artur melee attack with power 5
Minsk melee attack with power 15
Artur(lifePoints=5, manaPoints=3) won
simulateCombat(Goblin(), Minsk())
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Minsk(lifePoints=40) won

This pattern is powerful and finds usage in many cases. It has one problem though. Traits cannot hold state. They can be only used to reuse behavior. If we then come back to our subscriptions problem, we would be forced to implement subscriptions in every class that uses our trait:

interface RxJavaUser {
var subscriptions: List<Subscription>
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter(): RxJavaUser {
var subscriptions: List<Subscription> = listOf()
}

With transition to RxJava 2, we would be forced to change subscriptions definition in every class. Not good.

What is the best solution then? I am sorry, but I don’t have one perfect answer. We’ve seen a few different attempts and they all have some advantages and downsides. Although we’ve also seen how each of this attempts can be used in a very useful way. We’ve seen how composition helps us overcome inheritance problems and express healthier relation. We’ve seen how delegation can additionally give us polymorphic behavior. Finally, we’ve seen traits and how powerful they are when we need to express behavior similar to a few classes. I hope that you will have this all alternatives to inheritance in your mind because they are important tools that can make your code much better and more expressive.

About the author

Marcin Moskała (@marcinmoskala) is a trainer and consultant, currently concentrating on giving Kotlin in Android and advanced Kotlin workshops (fill out the form, so we can talk about your needs). He is also a speaker, author of articles and a book about Android development in Kotlin.

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.

I would like to thanks Geoff Falk for language review.

--

--

Kt. Academy creator, co-author of Android Development with Kotlin, author of open-source libraries, community activist. http://marcinmoskala.com/