1. 什么是委托?

委托,也就是委托模式,它是23种经典设计模式种的一种,又名代理模式,在委托模式中,有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项技巧,其他的几种设计模式如:策略模式、状态模式和访问者模式都是委托模式的具体场景应用。
委托模式中,有三个角色,约束、委托对象和被委托对象。

委托模式中的角色.png
image.png

  • 约束: 约束是接口或者抽象类,它定义了通用的业务类型,也就是需要被代理的业务
  • 被委托对象: 具体的业务逻辑执行者
  • 委托对象: 负责对真是角色的应用,将约束累定义的业务委托给具体的委托对象。
    2. 委托的具体场景
    上一节讲了委托的定义和它所包含的几个角色,那么具体该怎么运用呢?我们以一个实际的例子来看看。
    现在很多年轻人都爱完游戏,不管是吃鸡、王者荣耀还是英雄联盟。它们都是有等级之分的:青铜->白银->黄金->铂金->钻石->宗师->王者,等级越高,代表你越厉害,就拿英雄联盟来说,我们多数混迹在白银黄金阶段,要上钻石宗师段位非常困难。比如你排位打了很久,就差几场就能上宗师了,老是打不上去,这个时候怎么办呢?好办,现在有很多游戏代练,委托游戏代练给你打上去就好了。这其实就是一个委托模式。代码该怎么写呢?一起来看看:
    首先,我们定义约束类,定义我们需要委托的业务,就拿这个场景来说,我们的业务就是打排位赛,升级。因此,定义个约束类(接口)IGamePlayer:
    1. // 约束类
    2. interface IGamePlayer {
    3. // 打排位赛
    4. fun rank()
    5. // 升级
    6. fun upgrade()
    7. }
    约束类中,定义了我们要代理的业务rank()upgrade(),然后,我们就定义被委托对象,也就是游戏代练
    1. // 被委托对象,本场景中的游戏代练
    2. class RealGamePlayer(private val name: String): IGamePlayer{
    3. override fun rank() {
    4. println("$name 开始排位赛")
    5. }
    6. override fun upgrade() {
    7. println("$name 升级了")
    8. }
    9. }
    如上,我们定义了一个被委托对象RealGamePlayer, 它有一个属性name,它实现了我们约定的业务(实现了接口方法)。
    接下来,就是委托角色
    1. // 委托对象
    2. class DelegateGamePlayer(private val player: IGamePlayer): IGamePlayer by player
    我们定义了一个委托类DelegateGamePlayer, 现在游戏代练有很多,水平有高有低,如果发现水平不行,我们可以随时换,因此,我们把被委托对象作为委托对象的属性,通过构造方法传进去。

    注意:在kotlin 中,委托用关键字by 修饰,by后面就是你委托的对象,可以是一个表达式。因此在本例中,通过by player 委托给了具体的被委托对象。

最后,看一下场景测试类:

  1. // Client 场景测试
  2. fun main() {
  3. val realGamePlayer = RealGamePlayer("张三")
  4. val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
  5. delegateGamePlayer.rank()
  6. delegateGamePlayer.upgrade()
  7. }

我们定义了一个游戏代练,叫张三,将它传递给委托类,然后就可以开始排位和升级的业务了,而最终谁完成了排位赛和升级了,当然是我们的被委托对象,也就是游戏代练—张三。
运行,结果如下:

  1. 张三 开始排位赛
  2. 张三 升级了

小结:以上就是委托的应用,再来回顾一下它的定义:2个对象参与处理同一请求,这个请求就是我们约束类的逻辑,因此委托类(DelegateGamePlayer)和被委托类(RealGamePlayer)都需要实现我们的约束接口IGamePlayer

3. 属性委托

在Kotlin 中,有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们,但是很麻烦,各种样板代码存在,我们知道,Kotlin可是宣称要实现零样板代码的。为了解决这些问题呢?Kotlin标准为我们提供了委托属性

  1. class Test {
  2. // 属性委托
  3. var prop: String by Delegate()
  4. }

委托属性的语法如下:
val/var <属性名>: <类型> by <表达式>
跟我们前面将的委托类似,只不过前面是类委托,这里属性委托

3.1 属性委托的原理

前面讲的委托中,我们有个约束角色,里面定义了代理的业务逻辑。而委托属性呢?其实就是上面的简化,被代理的逻辑就是这个属性的get/set方法。get/set会委托给被委托对象setValue/getValue方法,因此被委托类需要提供setValue/getValue这两个方法。如果是val 属性,只需提供getValue。如果是var 属性,则setValue/getValue都需要提供。
比如上面的Delegate类:

  1. class Delegate {
  2. operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
  3. return "$thisRef, thank you for delegating '${property.name}' to me!"
  4. }
  5. operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
  6. println("$value has been assigned to '${property.name}' in $thisRef.")
  7. }
  8. }

其中的参数解释如下:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
  • property —— 必须是类型 KProperty<*>或其超类型。
  • value —— 必须与属性同类型或者是它的子类型。

测试如下:

  1. fun main() {
  2. println(Test().prop)
  3. Test().prop = "Hello, Android技术杂货铺!"
  4. }

打印结果如下:

  1. Test@5197848c, thank you for delegating 'prop' to me!
  2. Hello, Android技术杂货铺! has been assigned to 'prop' in Test@17f052a3.

3.2 另一种实现属性委托的方式

上面我们讲了,要实现属性委托,就必须要提供getValue/setValue方法,对于比较的同学可能就要说了,这么复杂的参数,还要每次都要手写,真是麻烦,一不小心就写错了。确实是这样,为了解决这个问题, Kotlin 标准库中声明了2个含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

  1. interface ReadOnlyProperty<in R, out T> {
  2. operator fun getValue(thisRef: R, property: KProperty<*>): T
  3. }
  4. interface ReadWriteProperty<in R, T> {
  5. operator fun getValue(thisRef: R, property: KProperty<*>): T
  6. operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
  7. }

被委托类 实现这两个接口其中之一就可以了,val 属性实现ReadOnlyPropertyvar属性实现ReadOnlyProperty

  1. // val 属性委托实现
  2. class Delegate1: ReadOnlyProperty<Any,String>{
  3. override fun getValue(thisRef: Any, property: KProperty<*>): String {
  4. return "通过实现ReadOnlyProperty实现,name:${property.name}"
  5. }
  6. }
  7. // var 属性委托实现
  8. class Delegate2: ReadWriteProperty<Any,Int>{
  9. override fun getValue(thisRef: Any, property: KProperty<*>): Int {
  10. return 20
  11. }
  12. override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
  13. println("委托属性为: ${property.name} 委托值为: $value")
  14. }
  15. }
  16. // 测试
  17. class Test {
  18. // 属性委托
  19. val d1: String by Delegate1()
  20. var d2: Int by Delegate2()
  21. }

如上代码所示,定义了2个属性代理,都通过 ReadOnlyProperty / ReadWriteProperty 接口实现。
测试代码如下:

  1. val test = Test()
  2. println(test.d1)
  3. println(test.d2)
  4. test.d2 = 100

打印结果:

  1. 通过实现ReadOnlyProperty实现,name:d1
  2. 20
  3. 委托属性为: d2 委托值为: 100

可以看到,与手动实现setValue/getValue效果一样,但是这样写代码就方便了很多了。

4. Kotlin 标准库中提供几个委托

Kotlin 标准库中提供了几种委托,例如:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

    4.1 延迟属性 lazy

    lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

    1. val lazyProp: String by lazy {
    2. println("Hello,第一次调用才会执行我!")
    3. "西哥!"
    4. }
    5. // 打印lazyProp 3次,查看结果
    6. fun main() {
    7. println(lazyProp)
    8. println(lazyProp)
    9. println(lazyProp)
    10. }

    打印结果如下:

    1. Hello,第一次调用才会执行我!
    2. 西哥!
    3. 西哥!
    4. 西哥!

    可以看到,只有第一次调用,才会执行lambda表达式中的逻辑,后面调用只会返回lambda表达式的最终值。

    4.1.1 lazy 也可以接受参数

    lazy延迟初始化是可以接受参数的,提供了如下三个参数:

    1. /**
    2. * Specifies how a [Lazy] instance synchronizes initialization among multiple threads.
    3. */
    4. public enum class LazyThreadSafetyMode {
    5. /**
    6. * Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
    7. */
    8. SYNCHRONIZED,
    9. /**
    10. * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
    11. * but only the first returned value will be used as the value of [Lazy] instance.
    12. */
    13. PUBLICATION,
    14. /**
    15. * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
    16. *
    17. * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
    18. */
    19. NONE,
    20. }

    三个参数解释如下:

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全

  • LazyThreadSafetyMode. PUBLICATION: 初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

使用如下:

  1. val lazyProp: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
  2. println("Hello,第一次调用才会执行我!")
  3. "西哥!"
  4. }

如果你指定的参数为LazyThreadSafetyMode.SYNCHRONIZED,则可以省略,因为lazy默认就是使用的LazyThreadSafetyMode.SYNCHRONIZED

4.2 可观察属性 Observable

如果你要观察一个属性的变化过程,那么可以将属性委托给Delegates.observable, observable函数原型如下:

  1. public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
  2. ReadWriteProperty<Any?, T> =
  3. object : ObservableProperty<T>(initialValue) {
  4. override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
  5. }

接受2个参数:

  • initialValue: 初始值
  • onChange: 属性值被修改时的回调处理器,回调有三个参数property,oldValue,newValue,分别为: 被赋值的属性、旧值与新值。

使用如下:

  1. var observableProp: String by Delegates.observable("默认值:xxx"){
  2. property, oldValue, newValue ->
  3. println("property: $property: $oldValue -> $newValue ")
  4. }
  5. // 测试
  6. fun main() {
  7. observableProp = "第一次修改值"
  8. observableProp = "第二次修改值"
  9. }

打印如下:

  1. property: var observableProp: kotlin.String: 默认值:xxx -> 第一次修改值
  2. property: var observableProp: kotlin.String: 第一次修改值 -> 第二次修改值

可以看到,每一次赋值,都能观察到值的变化过程。

4.2.1 vetoable 函数

vetoableobservable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效
来看这样一个例子:声明一个Int类型的属性vetoableProp,如果新的值比旧值大,则生效,否则不生效。
代码如下:

  1. var vetoableProp: Int by Delegates.vetoable(0){
  2. _, oldValue, newValue ->
  3. // 如果新的值大于旧值,则生效
  4. newValue > oldValue
  5. }

测试代码:

  1. fun main() {
  2. println("vetoableProp=$vetoableProp")
  3. vetoableProp = 10
  4. println("vetoableProp=$vetoableProp")
  5. vetoableProp = 5
  6. println("vetoableProp=$vetoableProp")
  7. vetoableProp = 100
  8. println("vetoableProp=$vetoableProp")
  9. }

打印如下:

  1. vetoableProp=0
  2. 0 -> 10
  3. vetoableProp=10
  4. 10 -> 5
  5. vetoableProp=10
  6. 10 -> 100
  7. vetoableProp=100

可以看到10 -> 5 的赋值没有生效。

4.3 属性存储在映射中

还有一种情况,在一个映射(map)里存储属性的值,使用映射实例自身作为委托来实现委托属性,如:

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

测试如下:

  1. fun main() {
  2. val user = User(mapOf(
  3. "name" to "西哥",
  4. "age" to 25
  5. ))
  6. println("name=${user.name} age=${user.age}")
  7. }

打印如下:

  1. name=西哥 age=25

使用映射实例自身作为委托来实现委托属性,可以使用在json解析中,因为json本身就可以解析成一个map。不过,说实话,我暂时还没有发现这种使用场景的好处或者优势,如果有知道的同学,评论区告知,谢谢!

5. 总结

委托在kotlin中占有举足轻重的地位,特别是属性委托,lazy延迟初始化使用非常多,还有其他一些场景,比如在我们安卓开发中,使用属性委托来封装SharePreference,大大简化了SharePreference的存储和访问。在我们软件开发中,始终提倡的是高内聚,低耦合。而委托,就是内聚,可以降低耦合。另一方面,委托的使用,也能减少很多重复的样板代码

参考:https://www.kotlincn.net/docs/reference/delegation.html