委派(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. 自訂屬性委派#
實作 ReadOnlyProperty 或 ReadWriteProperty 介面,可以建立自訂委派。
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