Note: see the link below for the English version of this article.

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

Delegation trong Kotlin và các hạn chế

Kotlin hỗ trợ việc implement pattern delegation một cách thuận tiện. Nó giúp ta không phải tự viết nhiều code. Nhưng ta cũng phải để ý tới những hạn chế của tính năng này. Nhất là khi ta muốn thay đổi object delegate tại runtime.

Delegation pattern trong Kotlin

Đây là sơ đồ UML của delegation pattern.

Sơ đồ UML của delegation pattern

Còn đây là code của interface Weapon cùng với 2 lớp GunSword implement interface này.

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")
    }
}

Trong phần tiếp theo, ta sẽ tự mình implement delegation pattern rồi sau đó thử dùng chức năng do Kotlin hỗ trợ sẵn.

Tự mình implement delegation pattern

Thông thường, ta sẽ implement delegation pattern như dưới đây.

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

Ta phải tự mình override hàm attack bằng cách gọi weapon.attack(). Sau đó ta kiểm tra xem WeaponWrapper có chạy đúng như ta mong muốn không.

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

Đoạn code trên sẽ in dòng sau ra console.

A gun can shoot. BANG BANG

Nhưng cách làm này không được scalable lắm. Bởi vì nếu interface Weapon có hàng chục hay hàng trăm hàm thì ta sẽ phải lần lượt override chúng từng cái một.

Tận dụng tính năng delegation trong Kotlin

Nếu ta dùng tính năng delegation của Kotlin thì code sẽ rất đơn giản.

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

Đó là tất cả những gì ta phải viết. Compiler sẽ tự implement tất cả các hàm trong interface Weapon bằng cách gọi hàm cùng tên trong delegate object (tức là weapon trong trường hợp này).

Delegate object là đặt cố định từ lúc khởi tạo

Thử đổi vũ khí tại runtime

Bây giờ ta sẽ thử đổi vũ khí tại runtime như dưới đây.

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

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

Vì ta muốn thay đổi property weapon của WeaponWrapper nên ta phải sửa lớp đó một chút.

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

Compiler sẽ ngay lập tức hiển thị cảnh báo dưới đây.

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

Và nếu ta thử chạy lại đoạn code ở trên, kết quả sẽ như sau.

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

Đây là điểm khác biệt so với khi ta tự mình implement delegation pattern. Nhưng vì sao lại có sự khác biệt này? Chả phải ta đã đổi sang dùng class Sword rồi hay sao? Ta sẽ tìm hiểu kỹ hơn về điểm này.

Cách hoạt động của delegation trong Kotlin

Đây là bytecode đầy đủ của lớp WeaponWrapper. Có thể thấy là compiler đã thay ta sinh ra rất nhiều bytecode chỉ từ một dòng lệnh.

Ta sẽ tập trung vào một vài phần quan trọng. Đầu tiên là bytecode để khởi tạo property weapon.

// <lược bỏ một số code>
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.$$delegate_0 : LWeapon;
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.weapon : LWeapon;
// <lược bỏ một số code>

Có thể thấy là tham số từ constructor được lưu vào trong 2 trường là WeaponWrapper.weaponWeaponWrapper.$$delegate_0.

Tiếp đến là bytecode để thay đổi property đó.

// <lược bỏ một số code>
LINENUMBER 8 L1
ALOAD 0
ALOAD 1
PUTFIELD WeaponWrapper.weapon : LWeapon;
RETURN
// <lược bỏ một số code>

Ta thấy là chỉ trường WeaponWrapper.weapon được cập nhật. Còn code để implement interface Weapon thì sao? Nó nằm ở đây.

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

Hàm attack được implement thông qua trường $$delegate_0, mà trường này chỉ được đặt một lần tại lúc khởi tạo. Vì thế mặc dù trường WeaponWrapper.weapon được cập nhật để lưu reference tới object của lớp Sword, WeaponWrapper.$$delegate_0 vẫn lưu reference tới object Gun ban đầu. Đây là lý do vì sao tao lại in dòng A gun can shoot. BANG BANG ra console tới 2 lần.

Kết thúc

Thông qua từ khoá by, Kotlin giúp ta implement Delegation pattern một cách dễ dàng. Đồng thời nó giúp hạn chế lỗi vì ta không phải tự override từng hàm một. Ta chỉ cần nhớ không thay đổi delegate object tại runtime. Và trong phần lớn các trường hợp, ta nên đặt weapon là immutable object (khai báo với val thay vì var).

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

Leave a Reply