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
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.
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.
One Thought on “Delegation in Kotlin and its limitations”