委派#

委派(Delegation) 是一種設計模式,讓一個物件將部分職責轉交給另一個物件處理。Kotlin 在語言層面內建支援委派,使用 by 關鍵字即可實現,不需要撰寫樣板程式碼。


1. 類別委派#

類別委派讓一個類別「繼承」介面的實作,但實際邏輯由另一個物件負責。

不使用委派的寫法(繁瑣)#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface Logger {
    fun log(message: String)
    fun warn(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("[LOG] $message")
    override fun warn(message: String) = println("[WARN] $message")
}

// 手動委派:每個方法都要轉發
class App(private val logger: ConsoleLogger) : Logger {
    override fun log(message: String) = logger.log(message)
    override fun warn(message: String) = logger.warn(message)
}

使用 by 委派(簡潔)#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Logger {
    fun log(message: String)
    fun warn(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("[LOG] $message")
    override fun warn(message: String) = println("[WARN] $message")
}

// by logger 自動轉發所有介面方法
class App(logger: Logger) : Logger by logger

fun main() {
    val app = App(ConsoleLogger())
    app.log("應用程式啟動")   // 輸出:[LOG] 應用程式啟動
    app.warn("記憶體用量偏高") // 輸出:[WARN] 記憶體用量偏高
}

覆寫部分方法#

使用委派後,仍可以覆寫特定方法,只讓其餘方法自動轉發。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Logger {
    fun log(message: String)
    fun warn(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("[LOG] $message")
    override fun warn(message: String) = println("[WARN] $message")
}

class FileLogger(private val logger: Logger) : Logger by logger {
    // 只覆寫 warn,log 仍由 logger 處理
    override fun warn(message: String) {
        println("[FILE-WARN] $message")
    }
}

fun main() {
    val logger = FileLogger(ConsoleLogger())
    logger.log("啟動")        // 輸出:[LOG] 啟動(轉發給 ConsoleLogger)
    logger.warn("磁碟空間不足") // 輸出:[FILE-WARN] 磁碟空間不足(自行處理)
}

2. 屬性委派#

屬性委派讓屬性的 get / set 邏輯由另一個物件(委派者)處理,使用語法為 val/var 屬性名稱 by 委派者


2.1 lazy:延遲初始化#

lazy 讓屬性在 第一次被存取時 才執行初始化,之後快取結果。適合初始化成本高的屬性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class DataProcessor {
    val heavyData: List<Int> by lazy {
        println("正在載入大量資料…")
        (1..1_000_000).toList()
    }
}

fun main() {
    val processor = DataProcessor()
    println("物件已建立")
    println("資料筆數:${processor.heavyData.size}") // 這時才初始化
    println("再次存取:${processor.heavyData.size}") // 直接使用快取
    // 輸出:
    // 物件已建立
    // 正在載入大量資料…
    // 資料筆數:1000000
    // 再次存取:1000000
}

2.2 observable:監聽屬性變更#

Delegates.observable 讓你在屬性值改變時執行自訂邏輯。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("(未設定)") { property, oldValue, newValue ->
        println("${property.name} 從「$oldValue」變更為「$newValue」")
    }
}

fun main() {
    val user = User()
    user.name = "Alice"
    user.name = "Bob"
    // 輸出:
    // name 從「(未設定)」變更為「Alice」
    // name 從「Alice」變更為「Bob」
}

2.3 vetoable:條件式攔截變更#

Delegates.vetoable 讓你在屬性值改變前做驗證,回傳 false 可拒絕變更。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlin.properties.Delegates

class AgeHolder {
    var age: Int by Delegates.vetoable(0) { _, oldValue, newValue ->
        if (newValue < 0) {
            println("拒絕:年齡不能為負數(嘗試設定 $newValue)")
            false // 拒絕
        } else {
            true  // 允許
        }
    }
}

fun main() {
    val holder = AgeHolder()
    holder.age = 25
    println("年齡:${holder.age}") // 輸出:年齡:25
    holder.age = -5
    println("年齡:${holder.age}") // 輸出:年齡:25(未被變更)
    // 輸出:
    // 年齡:25
    // 拒絕:年齡不能為負數(嘗試設定 -5)
    // 年齡:25
}

2.4 委派給 Map#

可以將屬性委派給 Map,適合解析 JSON 或設定檔等動態資料。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Config(private val map: Map<String, Any>) {
    val host: String by map
    val port: Int by map
    val debug: Boolean by map
}

fun main() {
    val config = Config(
        mapOf(
            "host" to "localhost",
            "port" to 8080,
            "debug" to true
        )
    )
    println("${config.host}:${config.port}") // 輸出:localhost:8080
    println("除錯模式:${config.debug}")      // 輸出:除錯模式:true
}

3. 自訂屬性委派#

實作 ReadOnlyPropertyReadWriteProperty 介面,可以建立自訂委派。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class NonNullDelegate<T>(private var value: T) : ReadWriteProperty<Any, T> {
    override fun getValue(thisRef: Any, property: KProperty<*>): T = value

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
        requireNotNull(value) { "${property.name} 不可設為 null" }
        this.value = value
    }
}

class User {
    var name: String by NonNullDelegate("未命名")
}

fun main() {
    val user = User()
    user.name = "Alice"
    println(user.name) // 輸出:Alice
}

4. 委派的應用場景#

場景建議使用的委派
高成本屬性延遲初始化lazy
監聽屬性變更(如 UI 更新)observable
屬性值驗證vetoable
從 Map 讀取設定Map 委派
介面實作轉發類別委派 by

Reference#

https://kotlinlang.org/docs/delegation.html