Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/kotlin-delegation-limit-vie

Delegation in Kotlin and its limitation

Kotlin has great support for implementing the delegation pattern. This helps us cut back on boilerplate code. But at the same time, we must be mindful of its limitations and pitfalls. Especially if we intend to change the delegate object at runtime.

Delegation pattern in Kotlin

This is the UML diagram of the delegation pattern.

UML diagram of the delegation pattern

Below is the Weapon interface and the Gun and Sword classes, which implement Weapon.

interface Weapon {
    fun attack()
}

class Gun : Weapon {
    override fun attack() {
        println("A gun can shoot. BANG BANG")
    }
}

class Sword : Weapon {
    override fun attack() {
        println("A sword can slash. Woosh")
    }
}

In the following sections, we will implement the delegation pattern first manually and then using Kotlin delegation.

Implement the delegation pattern manually

Traditionally, this is how we implement the delegation pattern.

class WeaponWrapper(private val weapon: Weapon) : Weapon {
    override fun attack() = weapon.attack()
}

We have to override the attack method ourselves by calling weapon.attack(). We can easily verify that WeaponWrapper behaves as expected.

val gun = Gun()
val wrapper = WeaponWrapper(gun)
wrapper.attack()

This will print the following message to the console.

A gun can shoot. BANG BANG

However, this is not scalable. Because if the Weapon interface has dozens or hundreds of methods, we will have to manually override them one-by-one.

Take advantage of Kotlin delegation

In contrast, by using Kotlin delegation, we can simplify the code above like this.

class WeaponWrapper(private val weapon: Weapon) : Weapon by weapon

And that’s it, the compiler will implement all methods in the Weapon interface for us, by forwarding to the same method in the delegate object (weapon in this case).

The delegate object is fixed at initialization

Switching weapons at runtime

Now let’s say we want to switch the weapon at runtime like this.

val gun = Gun()
val wrapper = WeaponWrapper(gun)
wrapper.attack()

val sword = Sword()
wrapper.weapon = sword
wrapper.attack()

Because we want to change the weapon property inside WeaponWrapper, we must make a small change to this class.

class WeaponWrapper(var weapon: Weapon) : Weapon by weapon

Instantly, the compiler gives us the following warning.

Delegating to 'var' property does not take its changes into account

And if we run the code above, we will get the following result.

A gun can shoot. BANG BANG
A gun can shoot. BANG BANG

This is different from when we implement the delegation pattern manually. But why is that? Didn’t we switch to using a sword for the second call? Let’s take a dive into the issue above.

How Kotlin delegation works

Here is the full bytecode of the WeaponWrapper class. It’s amazing to look at the amount of bytecode that corresponds to just one line of code (which the compiler writes for us).

We will look at the important parts. First is the bytecode to initialize the weapon property.

// <omitted>
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.$$delegate_0 : LWeapon;
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.weapon : LWeapon;
// <omitted>

We can see that the same constructor argument is stored in two different fields, WeaponWrapper.weapon and WeaponWrapper.$$delegate_0.

Next is the bytecode to update that property.

// <omitted>
LINENUMBER 8 L1
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.weapon : LWeapon;
RETURN
// <omitted>

We can see that only the WeaponWrapper.weapon field is updated. And what about the code to implement Weapon interfaces? Here it is.

public attack()V
 L0
  ALOAD 0
  GETFIELD WeaponWrapper1.$$delegate_0 : LWeapon;
  INVOKEINTERFACE Weapon.attack ()V (itf)
  RETURN
 L1
  LOCALVARIABLE this LWeaponWrapper1; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1

The attack method is implemented via the $$delegate_0 field, which is set just once at creation time. So even though the WeaponWrapper.weapon field now holds a reference to the Sword object, WeaponWrapper.$$delegate_0 still holds the old reference to the Gun object. This is why we printed A gun can shoot. BANG BANG twice to console.

Conclusion

With the by keyword, Kotlin makes implementing the Delegation pattern easy. While at the same time cutting back on errors because we don’t have to manually override methods one by one. We just need to remember that we can’t switch the delegate object at runtime. And in most cases, we should just stick with an immutable weapon object (using val instead of var) anyway.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Delegation in Kotlin and its limitations”

Leave a Reply