Kotlin "By" Class Delegation: Favor Composition Over Inheritance

When people ask me why Kotlin over Java, I often say because Kotlin is a better Java. You get more than half of Effective Java implemented for you. In charter 4 of the book, the author lists many items about inheritance because it is very error prone. The most well known item probably would be Item 16: Favor composition over inheritance. To implement that item, Kotlin introduces a special key work by to help you with implementing delegation in a single line.

Some side notes:

  • by keyword can be used with properties as well. I will just focus on class delegation in this post and maybe do another one for it.
  • Kotlin also has some other language features to “reduce” the use of inheritance like typealias for tagging classes and class are final by default.

Inheritance can be problematic

Before jumping directly into the by keyword, let’s try to understand the why first. This is always the important part of learning new things. Without knowing what problems the tool tries to solve, you will pretty much end up using the tool for “being cool”.

problem 1: indeterministic inherited behaviors

I took the example from Fragmented episode #87. The podcast is awesome, and I recommend Android developers to listen to it.

For some reasons you try to extending a standard collection to change its default behavior. Maybe you want to count how many items are added into a set, and here is how you would probably implement it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CountingSet extends HashSet<Long> {

    private long addedCount = 0;

    @Override
    public boolean add(Long aLong) {
        addedCount++;
        return super.add(aLong);
    }

    @Override
    public boolean addAll(Collection<? extends Long> c) {
        addedCount = addedCount + c.size();
        return super.addAll(c);
    }

    public long getAddedCount() {
        return addedCount;
    }
}

Simple, right? All you need to is to extend the existing HashSet to avoid reinvent the wheel and increment the addedCount every time either add() or addAll() is called. It seems very promising but when you actually try to get the addedCount with following example usage, you will find it surprising:

1
2
3
val countingSet = CountingSet()
countingSet.addAll(setOf(1, 3, 4))
println("added count is ${countingSet.addedCount}") // prints: added count is 6

The problem here is inside the HashSet#addAll():

1
2
3
4
5
6
7
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

It iterates over the collection c and calls add() method for each element, so you end up counting each number twice.

You might argue since we know the implementation details of HashSet, we can just increment the counter inside add(). That would indeed solve this particular problem, but it is still problematic since you don’t own the HashSet class and therefore you cannot guarantee it would never change.

problem 2: initialization order in constructor

This might not be directly related to the by keyword, but it is something worth to mention here.

Sometimes it’s very attempting to write some “template methods” in an abstract class and calling them at constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
abstract class Animal {

    init {
        println("I'm ${getName()}")
    }

    abstract fun getName(): String
}

class Dog(private val name: String) : Animal() {

    override fun getName(): String = name
}

fun main(args: Array<String>) {
    val dog = Dog("Puff") // prints: I'm null
}

The reason getName() returns null is because in java super(), the constructor from super class, must be called first. Therefore when line println("I'm ${getName()}") is executed, the property name in Dog has not been assigned yet.

This is a common issue, and even IntelliJ will try to warn you:

warnings

With many potential issues, you should just avoid inheritance and start program to interfaces not implementations when possible.

Delegation in Java

I want to include some examples in Java to show delegation is already widely used in many libraries and frameworks.

Not surprisingly [Guava][https://github.com/google/guava], as the core libraries in Google, has implemented a sets of Forwarding collections to help us to implement common collection interfaces without extending them.

CountingSet v2

The same counting set can be rewritten with ForwardingSet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CountingSet extends ForwardingSet<Long> {

    private final Set<Long> delegate = Sets.newHashSet();
    private long addedCount = 0L;

    @Override
    protected Set<Long> delegate() {
        return delegate;
    }

    @Override
    public boolean add(Long element) {
        addedCount++;
        return delegate().add(element);
    }

    @Override
    public boolean addAll(Collection<? extends Long> collection) {
        addedCount += collection.size();
        return delegate().addAll(collection);
    }
}

The implementation looks similar to the previous one, but this time we are not dependent on the actual implementation of add() and addAll() since we just “forward” the method calls to the delegate(). We can safely replace the delegate with other types without knowing its implementation details.

It is still an abstract class because Java does not support “delegation” at language level like Kotlin does.

Another delegation we use a lot as Android developers is in AppCompatActivity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * @return The {@link AppCompatDelegate} being used by this Activity.
 */
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

// inside AppCompatDelegate.java
private static AppCompatDelegate create(Context context, Window window,
        AppCompatCallback callback) {
    if (Build.VERSION.SDK_INT >= 24) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (Build.VERSION.SDK_INT >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (Build.VERSION.SDK_INT >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (Build.VERSION.SDK_INT >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
}

It follows the same pattern. It creates and “delegates” different implementation of AppCompatDelegate.

Delegation in Kotlin

The actual sample code here is going to be very short since Kotlin supports class delegation with by keyword, and we can have delegation in one line.

CountingSet v3

In kotlin the same counting set is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class CountingSet(private val delegate: MutableSet<Long> = HashSet()) : MutableSet<Long> by delegate {

    private var addCount = 0L

    override fun add(element: Long): Boolean {
        addCount++
        return delegate.add(element)
    }

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

A few things to know here:

#1 The target type must be an interface. It cannot be an abstract class.

#2 The delegate can be a primary constructor property or a new instance. For example you can create a set by writing just class MySet : Set<Long> by HashSet() In our example we need to call the delegate in two overridden functions so we make it a private property.

#3 You can have more than one delegations. For example:

1
2
3
4
5
6
7
8
class MySetMap : Set<Long> by HashSet(), Map<Long, Long> by HashMap() {
    override val size: Int
        get() = TODO("not implemented")

    override fun isEmpty(): Boolean {
        TODO("not implemented")
    }
}

would work just fine, but since both Map and Set define size and isEmpty(), you would need to implement those two methods so that the compiler won’t get confused.

Thoughts

Kotlin introduces the by keyword and it makes delegation so easy to implement. We should challenge ourselves to avoid inheritance more by using the language features.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Content written by Eric Lin with passions in programming
Built with Hugo
Theme Stack designed by Jimmy