委托模式:类委托、属性委托、标准委托、Map委托

委托模式,说白了就是「我不想干这活,交给别人干」。在 Kotlin 里,这个模式被语言层面直接支持了,用起来特别顺手。我个人非常喜欢这个特性,因为它能帮我们写出更干净、更灵活的代码。

你想想看,传统 Java 里要实现委托,你得手动写一堆转发方法,又臭又长。Kotlin 直接用 by 关键字搞定,编译器帮你生成那些模板代码。嗯,这就是 Kotlin 的务实之处。

类委托:把实现交给别人

类委托的意思是:一个类实现某个接口,但实际干活的是另一个对象。我举个实际例子——

假设我们有个 Logger 接口:

interface Logger {
    fun log(message: String)
    fun logError(message: String)
}

传统做法是每个实现类都自己写一遍方法体。但用委托,你可以这样:

class ConsoleLogger : Logger {
    override fun log(message: String) = println("INFO: $message")
    override fun logError(message: String) = println("ERROR: $message")
}

class FileLogger : Logger {
    override fun log(message: String) { /* 写入文件 */ }
    override fun logError(message: String) { /* 写入文件 */ }
}

// 委托给 ConsoleLogger
class AppLogger(logger: Logger) : Logger by logger

fun main() {
    val logger = AppLogger(ConsoleLogger())
    logger.log("启动成功")  // 实际调用 ConsoleLogger.log()
}

这里 AppLogger 自己一行代码都没写,所有接口方法都委托给了传入的 logger 对象。我在项目中经常用这个模式来做「装饰器」——比如给日志加时间戳,只需要在委托类外面包一层:

class TimestampLogger(private val wrapped: Logger) : Logger by wrapped {
    override fun log(message: String) {
        wrapped.log("[${System.currentTimeMillis()}] $message")
    }
}
我的习惯:类委托特别适合「组合优于继承」的场景。你不需要搞复杂的继承树,用委托就能灵活组合行为。

属性委托:懒加载、观察者、拦截器

属性委托是 Kotlin 委托模式的精髓。一个属性的 getter/setter 可以委托给另一个对象来处理。说白了,就是「这个属性的读写逻辑,我交给别人管」。

语法很简单:

class Example {
    var name: String by Delegate()
}

这里的 Delegate 需要实现 getValuesetValue 操作符。不过日常开发中,我们很少自己写委托类,因为 Kotlin 标准库已经提供了几个非常实用的标准委托。

标准委托:lazy、observable、vetoable

这三个是我用得最多的标准委托,几乎每个项目都会碰到。

lazy:懒加载

lazy 委托用于「第一次访问时才初始化」的场景。我经常用它来加载配置、创建重量级对象。

class ConfigManager {
    val config: Config by lazy {
        println("加载配置...")
        loadConfigFromFile()  // 耗时操作
    }
}

fun main() {
    val manager = ConfigManager()
    println("对象已创建")  // 此时 config 还没加载
    println(manager.config)  // 第一次访问,触发加载
    println(manager.config)  // 第二次访问,直接返回缓存值
}

输出结果:

对象已创建
加载配置...
Config(...)
Config(...)
关键点:lazy 默认是线程安全的。如果你确定只在单线程中使用,可以传 LazyThreadSafetyMode.NONE 提升性能。

我曾经在项目中遇到过一个坑:用 lazy 委托的属性,如果初始化代码里抛异常,第二次访问时会重新尝试初始化。嗯,这个行为其实挺合理的,但如果你没注意到,可能会觉得是 bug。

observable:观察属性变化

observable 委托让你在属性值变化时收到回调。我常用它来做数据绑定、日志记录。

class UserViewModel {
    var userName: String by Delegates.observable("初始值") { prop, old, new ->
        println("${prop.name} 从 $old 变为 $new")
        // 可以在这里更新 UI、发送事件等
    }
}

fun main() {
    val vm = UserViewModel()
    vm.userName = "Alice"  // 输出:userName 从 初始值 变为 Alice
    vm.userName = "Bob"    // 输出:userName 从 Alice 变为 Bob
}

注意,observable 是在值已经改变之后才回调。如果你想在赋值之前做拦截,需要用 vetoable

vetoable:带否决权的观察者

vetoableobservable 很像,但多了一个「否决权」。回调返回 true 表示允许修改,返回 false 表示拒绝修改。

class TemperatureController {
    var temperature: Int by Delegates.vetoable(25) { _, old, new ->
        // 温度不能超过 40 度
        if (new > 40) {
            println("温度 $new 过高,拒绝修改")
            false
        } else {
            println("温度从 $old 变为 $new")
            true
        }
    }
}

fun main() {
    val controller = TemperatureController()
    controller.temperature = 30  // 成功:温度从 25 变为 30
    controller.temperature = 50  // 失败:温度 50 过高,拒绝修改
    println(controller.temperature)  // 输出 30,没有被改成 50
}
避坑指南:我曾经在表单验证里用 vetoable 做输入拦截,结果发现它只能阻止属性赋值,但不能阻止对象内部状态的改变。如果你需要更复杂的验证逻辑,建议结合 require 或自定义 setter 一起使用。

Map 委托:动态属性

Map 委托是我觉得 Kotlin 最巧妙的设计之一。它允许你把一个类的属性映射到 Map 的键值对上。说白了,就是「对象的属性就是 Map 的 key,属性的值就是 Map 的 value」。

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

fun main() {
    val data = mapOf(
        "name" to "张三",
        "age" to 28,
        "email" to "zhangsan@example.com"
    )
    val person = Person(data)
    println(person.name)   // 张三
    println(person.age)    // 28
    println(person.email)  // zhangsan@example.com
}

这个模式在解析 JSON、处理配置数据时特别有用。你想想看,从 API 拿到的数据通常就是 Map 结构,用 Map 委托可以直接映射成类型安全的对象,省去了手动解析的麻烦。

如果你需要可变属性,用 MutableMap 即可:

class MutablePerson(map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int by map
}

fun main() {
    val data = mutableMapOf("name" to "李四", "age" to 30)
    val person = MutablePerson(data)
    person.age = 31
    println(data["age"])  // 31,Map 也跟着变了
}
我的经验:Map 委托非常适合做「配置对象」。比如从 YAML 或 properties 文件读取配置,直接映射成配置类,代码会非常简洁。但要注意类型安全——Map 里的值类型必须和属性类型匹配,否则运行时会抛异常。

知识体系总览

下面这张图帮你理清委托模式的整体脉络:

Kotlin 委托模式 类委托 by 关键字 组合优于继承 装饰器模式 属性委托 getValue / setValue 自定义委托 标准委托 Map 委托 Map / MutableMap JSON 解析 配置管理 lazy 懒加载 observable 观察者 vetoable 否决权

总结一下

委托模式在 Kotlin 里不是锦上添花,而是实实在在能减少样板代码、提升代码质量的手段。我个人建议你从 lazyobservable 开始用起,这两个最常用也最容易上手。等你熟悉了,再尝试自定义委托和 Map 委托。

嗯,委托模式就讲到这里。记住一句话:能用委托解决的问题,就别自己手写模板代码。Kotlin 帮你省下的时间,拿去喝杯咖啡不好吗?


公众号:蓝海资料掘金营,微信 deep3321