Note: see the link below for the English version of this article.
https://duongnt.com/kotlin-delegation-limit
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.
Còn đây là code của interface Weapon
cùng với 2 lớp Gun
và Sword
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.weapon
và WeaponWrapper.$$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
).