Kotlin 委派 (Delegation)

委派 (Delegation) 是一種設計模式,將某個物件的工作「外包」給另一個物件處理。 Kotlin 原生支援了 Class DelegationProperty Delegation,讓這種模式變得超級簡單,核心關鍵字就是 by

Class Delegation (類別委派)

這是一種取代「繼承」的好方法 (Composition over Inheritance)。 假設你想實作一個 List,但只想修改其中幾個方法的行為,其他都照舊。

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}

// Derived 類別實作 Base 介面,但它把所有工作都「委派」給 b 這個物件
class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print() // 輸出 10
}

你不需要手動寫 override fun print() { b.print() },編譯器幫你做掉了。

Property Delegation (屬性委派)

這是 Kotlin 最神奇的功能之一。 你可以把屬性的 get()set() 邏輯外包給一個代理物件。

語法:val/var <property name>: <Type> by <expression>

延遲初始化 lazy

最常用的標準委派。變數只會在 第一次被存取時 才執行初始化程式碼。

val heavyData: String by lazy {
    println("Computing...")
    // 模擬耗時操作
    Thread.sleep(1000)
    "Result"
}

fun main() {
    println("Start")
    println(heavyData) // 印出 "Computing..." 然後 "Result"
    println(heavyData) // 直接印出 "Result" (不會再計算)
}

觀察者 observable

當屬性值發生改變時,會自動執行 callback。

var name: String by Delegates.observable("Initial") { prop, old, new ->
    println("$old -> $new")
}

fun main() {
    name = "Miko" // 輸出 Initial -> Miko
    name = "Jason" // 輸出 Miko -> Jason
}

Map 委派 map

可以直接用 Map 來儲存屬性值 (常用於解析 JSON 或動態設定)。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))
println(user.name) // John Doe

否決賦值 vetoable

如果你希望在指派屬性新值之前進行檢查,決定是否接受這個新值,可以使用 vetoable。它類似 observable,但 Callback 必須回傳一個 Boolean。如果回傳 true,新值才會被接受;回傳 false 則維持原值。

var max: Int by Delegates.vetoable(0) { prop, old, new ->
    new > old // 只有當新值大於舊值時才更新
}

fun main() {
    println(max) // 0
    max = 10
    println(max) // 10 (10 > 0,更新成功)
    max = 5
    println(max) // 10 (5 < 10,更新被否決,維持原值)
}

非空委派 notNull

有些屬性因為某些原因無法在建構式中初始化(例如需要依賴外部注入),但你又不希望它宣告為 Nullable (Type?)。這時候可以使用 Delegates.notNull()

它類似 lateinit,但 notNull 實作上是利用委派,可以用於 Primitive Types (Int, Double 等),而 lateinit 不行。

var config: String by Delegates.notNull()

fun main() {
    // println(config) // 如果在賦值前讀取,會拋出 IllegalStateException
    config = "Loaded"
    println(config) // Loaded
}

Custom Delegation (自定義委派)

如果標準庫提供的委派不滿足需求,你也可以自己寫! 只要一個類別實作了 operator fun getValue (如果是 var 屬性則還需要 setValue),它就可以當作委派物件。

步驟 1:建立委派類別

實作 ReadOnlyPropertyReadWriteProperty 介面,不僅讓程式碼更標準,IDE 也會提供型別檢查輔助。

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class TrimDelegate : ReadWriteProperty<Any?, String> {
    private var trimmedValue: String = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return trimmedValue
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        // 自動去除頭尾空白
        trimmedValue = value.trim()
        println("Set value to '$trimmedValue'")
    }
}

步驟 2:使用它

class Post {
    var content: String by TrimDelegate()
}

fun main() {
    val post = Post()
    post.content = "   Hello World!   "
    println("Content: '${post.content}'") 
    // 輸出:
    // Set value to 'Hello World!'
    // Content: 'Hello World!'
}

這在封裝重複的 Getter/Setter 邏輯(如格式化、驗證、Log 記錄)時非常強大。

總結

  • by 關鍵字:一切委派的核心。
  • by lazy:最常用,省資源神器。
  • Delegation Pattern:用組合取代繼承,降低耦合。