[Kotlin] Delegation

代理模式是23种经典设计模式之一,代理模式被认为是继承的更好替代解决方案;因为代理比继承更加灵活,在Java语言中,通过反射可以实现动态代理,动态代理可以实现AOP编程,即:可以动态地往已有类中添加逻辑;比如:实现事务的自动提交,异常的自动捕获,热修复等等;

在Kotlin语言中,代理模式是默认支持的,不需要任何额外的代码,你只需要记住一个关键字by。我们不妨来试一下:

interface Base {
    fun sayHi()
}

class BaseImpl : Base {
    override fun sayHi() {
        println("BaseImpl->sayHi")
    }
}
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl()
    val derived = Derived(b)
    derived.sayHi()
}

这里Derived作为BaseImpl的代理类,拥有BaseImpl类中的所有方法,Derived将代理BaseImpl类执行BaseImpl类中的所有方法,就像继承自BaseImpl类一样。这样说起来有点抽象,来看一下Kotlin编译器具体为我们做了一些什么。但是,怎么看呢?教大家一个方法!
大家都知道,Kotlin和Java均是JVM语言,最终均转换到同样的Java字节码,这样我们就可以先将Kotlin编译为.class文件,再反编译为.java文件,看看对应的Java代码,我们就可以看到更多的细节。下面是最终反编译生成的Java代码:

public final class Derived implements Base {
  public Derived(@NotNull Base b) {
    this.$$delegate_0 = b;
  }
  public void sayHi() {
    this.$$delegate_0.sayHi();
  }
}

这里,我们可以清楚地看到,Kotlin编译器为我们动态添加了一个成员变量$$delegate_0,这个成员变量代表被代理的对象,这里对应的是BaseImpl对象,Derived里面的sayHi方法最终调用是代理对象的sayHi方法,即Kotlin编译器帮我们提供了一个非常漂亮的代理模式实现。

代理属性

在一些情况下,我们可能希望某些属性延迟加载,即在我们正在需要的时候才对它赋值;亦或者我们希望可以随时监听属性值的变化;在上述这些场景中,代理属性就可以发挥作用了。

代理属性的语法格式如下:

class DelegateProperty {
    val d: String by Delegate()
}
class Delegate {
    operator fun getValue(thisRef: Any? , property: KProperty<*>): String {
        return "Invoke getValue() , thisRef = $thisRef , property name = ${property.name}"
    }
    operator fun setValue(thisRef: Any? , property: KProperty<*> , value: String) {
        println("Invoke setValue() , thisRef = $thisRef , property name = ${property.name} , value = $value")
    }
}

fun main(args: Array<String>) {
    val dp = DelegatedProperty()
    dp.d = "Value0" // Invoke setValue() , thisRef = DelegatedProperty@2ef1e4fa , property name = d , value = Value0
   
    println(dp.d) // Invoke getValue() , thisRef = DelegatedProperty@2ef1e4fa , property name = d
}

这里的代理是如何实现的呢?我们知道,Kotlin的属性值会自动生成set/get方法,而代理类通过代理set/get方法生成相应的代理方法,这里的方法对应关系如下:

// thisRef对应代理对象的引用,property对应代理属性的反射属性封装
// 注意这里的代理方法一定要添加operator关键字,operator关键字是重载操作符关键字,后续的文章中会讲到,敬请期待
get() -> operator fun getValue(thisRef: Any? , property: KProperty<*>)
set() -> operator fun setValue(thisRef: Any? , property: KProperty<*> , value: T)

Kotlin标准库提供了一些常用代理的方法实现,即上文提到的几种代理,先来看第一种:延迟加载。

延迟加载

Kotlin提供了一个lazy方法用于实现延迟加载,lazy方法有一个lambda表达式参数,用于对属性进行初始化赋值,而一旦完成赋值,该lambda表达式将不会再次调用。lambda表达式调用发生在第一次使用该属性的时候,即实现了属性赋值的延迟加载。来看一个简单的例子:

// 使用标准库实现的lazy函数,实现属性的延迟加载
private val lazyValue: String by lazy {
    println("调用该初始赋值表达式完成赋值")
    // 这里是实际赋值
    "Hello, world"
}
fun main(args: Array<String>) {
    // 仅在第一次会调用lazy方法的lambda表达式
    println(lazyValue) // 打印:调用该初始赋值表达式完成赋值
    println(lazyValue) // 打印: Hello, world, 再次调用将不再调用lambda表达式
}

lazy方法是一个线程安全的延迟加载方法,为了加深大家的理解,根据上面的原理,我们尝试自己来实现一个非线程安全的延迟加载方法,看具体实现:

private object UNINITIALIZE_VALUE

class MyLazy<T>(initialize: ()->T) {
    private var value: Any? = UNINITIALIZE_VALUE
    private val initialize = initialize
    operator fun getValue(thisRef: Any? , property: KProperty<*>): T {
        if(value == UNINITIALIZE_VALUE) {
            value = initialize()
        }
        return value as T
    }
    operator fun setValue(thisRef: Any? , property: KProperty<*> , value: T) {
        this.value = value
    }
}
// 为了和标准库区分,使用__lazy命名
fun <T>  __lazy(initialize: () -> T): MyLazy<T> = MyLazy(initialize)

var lazyValue1 by __lazy {
    println("自定义lazy初始化赋值表达式被调用")
    "Hello , world"
}

fun main(args: Array<String>) {
    // 自定义延迟加载函数__lazy
    println(lazyValue1)
    lazyValue1 = "Other value"
    println(lazyValue1)
}

由此可见,实现一个延迟加载接口并不复杂,最重要的是要理解延迟加载的过程以及实现原理。总结实现延迟加载接口,需要注意三个地方:

  • 需要提供初始化lambda表达式参数,用于初始赋值
  • 需要实现代理属性对象的setValue/getValue方法,如果是val则只需要实现getValue即可
  • 需要严格确保属性不会被多次初始化

Observable属性

Kotlin标准库还提供了一个可观察属性,这个属性使用观察者模式实现,如果属性值发生变化则会调用相应的回调lambda接口通知使用者,先看一个具体的例子:

var observableValue by Delegates.observable("Initial value") {  prop , old , new ->
    println("$old -> $new")
}

fun main(args: Array<String>) {
    println(observableValue)    // 打印:Initial value
    observableValue = "Hello"   // 打印: Initial value -> Hello
    println(observableValue)    // 打印:Hello
}

这里的具体实现,感兴趣的同学请参看文章开头的方法进行追踪!

Storing Properties in a Map

这也是Kotlin标准库提供的一个非常有用的特性,它主要用于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
))

该方法比较简单,这里就不再赘述了!

总结

至此,关于代理的介绍可以暂时告一段落了!
代理模式是一个非常经典设计模式,在解决某些问题中可以发挥事半功倍的效果。幸运的是,Kotlin语言原生支持代理模式,实现代理模式如同声明一个属性一样简单。而且,代理模式的设计也非常漂亮,仅仅使用一个关键字by极尽简约之美。在日常编码中,一定要灵活运用代理模式,比如实现延迟加载,实现属性观察等等。KotterKnife 是一个非常经典的代理模式的实现例子,有兴趣的同学可以clone该仓库,查看源码,领会代理模式的优美。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

文章源码地址

Kotliner: ** https://github.com/yuanhoujun/Kotliner ** 别忘了点击仓库右上方的star哦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容