委托是常见的模式,它和编程语言无关,即把本来自己做的事情委托给另一个对象去做。装饰者模式和代理模式都通过委托复用了行为。Kotlin 在语言层面支持了委托,这一篇结合实例介绍一下 Kotlin 的委托。
Kotlin 的装饰者模式
装饰者模式和继承拥有相同的目的,都是为了扩展类,只不过它运用了更复杂的方式通:继承 组合。装饰者模式在复用原有类型和行为的基础上为其扩展功能。
下面是装饰者模式的实例:
代码语言:javascript复制interface Accessory {
fun name(): String // 配件名字
fun cost(): Int // 配件价格
fun type(): String // 配件类别
}
这个接口用来描述一个抽象的配件,一个具体的配件需要实现三个方法,分别来定义配件名字、价格、类别。
羽毛、戒指、耳环是3个具体的配件,它的实现如下:
代码语言:javascript复制class Feather: Accessory{
override fun name(): String = "Feather"
override fun cost(): Int = 20
override fun type(): String = "body accessory"
}
class Ring: Accessory{
override fun name(): String = "Ring"
override fun cost(): Int = 30
override fun type(): String = "body accessory"
}
class Earrings: Accessory{
override fun name(): String = "Earrings"
override fun cost(): Int = 15
override fun type(): String = "body accessory"
}
现需要新增羽毛戒指和羽毛耳环,按照继承的思想可以这样实现:
代码语言:javascript复制class FeatherRing: Accessory{
override fun name(): String = "FeatherRing"
override fun cost(): Int = 35
override fun type(): String = "body accessory"
}
class FeatherEarrings: Accessory{
override fun name(): String = "FeatherEarrings"
override fun cost(): Int = 45
override fun type(): String = "body accessory"
}
这样写的缺点是只复用了类型,没复用行为。每次新增类型的时候都得新增一个子类,会造成子类膨胀。若改用装饰者模式,则可以减少一个子类:
代码语言:javascript复制class Feather(private var accessory: Accessory) : Accessory {
override fun name(): String = "Feather" accessory.name()
override fun cost(): Int = 20 accessory.cost()
override fun type(): String = accessory.type()
}
现在羽毛戒指和耳环分别可以这样表达Feather(Ring())、Feather(Earrings())。
Feather运用组合持有了一个抽象的配件,这样被注入配件的行为就得以复用。name()和cost()在复用行为的基础上追加了新的功能,而type()直接将实现委托给了accessory。
运用 Kotlin 的委托语法可以进一步简化Feather类:
代码语言:javascript复制class Feather(private var accessory: Accessory): Accessory by accessory {
override fun name(): String = "Feather" accessory.name()
override fun cost(): Int = 20 accessory.cost()
}
by 关键词出现在类名后面,表示类委托,即把类的实现委托一个对象,该对象必须实现和类相同的接口,在这里是Accessory接口。使用by的好处是消灭模板代码,就如上面所示,type()接口的实现就可以省略。
惰性初始化一次
惰性初始化也是一种常见的模式:延迟对象的初始化,直到第一次访问它。当初始化消耗大量资源,惰性初始化显得特别有价值。
支持属性是一种实现惰性初始化的惯用技术:
代码语言:javascript复制class BitmapManager {
// 支持属性用于存储一组 Bitmap
private var _bitmaps: List<Bitmap>? = null
// 供外部访问的一组 Bitmap
val bitmaps: List<Bitmap>
get() {
if (_bitmaps == null) {
_bitmaps = loadBitmaps()
}
return _bitmaps!!
}
}
支持属性_bitmaps是私有的,它用来存储一组 Bitmap,而另一个同样类型的bitmaps用来提供一组 Bitmap 的访问。
这样只有当第一次访问BitmapManager.bitmaps时,才会去加载 Bitmap。第二次访问时,也不会重新加载 Bitmap,可直接返回_bitmap。
上面这段代码就是 Kotlin 预定义函数lazy()内部运用的技术,有了它就可以消灭模板代码:
代码语言:javascript复制class BitmapManager {
val bitmaps by lazy { loadBitmaps() }
}
这里的关键词by出现在属性名后面,表示属性委托,即将属性的读和写委托给另一个对象,被委托的对象必须满足一定的条件:
- 对于 val 修饰的只读变量进行属性委托时,被委托的对象必须实现getValue()接口,即定义如何获取变量值。
- 对于 var 修饰的读写变量进行属性委托时,被委托对象必须实现getValue()和setValue()接口,即定义如何读写变量值。
属性委托的三种实现方式
lazy()方法的返回值是一个Lazy对象:
代码语言:javascript复制public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
Lazy类并没有直接实现getValue()方法。它使用了另一种更加灵活的方式:
代码语言:javascript复制public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
getValue()被声明为Lazy类的扩展函数。这是 Kotlin 独有的在类体外为类新增功能的特性。在原有类不能被修改的时候,特别好用。
除了扩展函数,还有另外两种方式可以实现被委托类(假设代理的类型为 String):
代码语言:javascript复制class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Delegate"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
}
这种方式新建了一个代理类,并且在类中通过关键词operator重载了getValue()和setValue()这两个运算符,分别对应取值和设置操作。
最后一种方式如下(假设代理的类型为 String):
代码语言:javascript复制class Delegate : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Delegate"
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
}
即实现ReadWriteProperty接口中的getValue()和setValue()方法。
然后就可以像这样使用代理类:
代码语言:javascript复制class Test {
var str: String by Delegate()
}
属性委托背后的实现如下:
代码语言:javascript复制class Test {
private delegate = Delegate()
var str : String
get () = delegate.getValue(this, kProperty)
set (value: String) = delegate.setValue(this, kProperty, value)
}
新建的Delegate类会被存储到一个支持属性delegate中,委托属性的设置和取值方法的实现全权委托给代理类。
委托之后,当访问委托属性时就好比在调用代理类的方法:
代码语言:javascript复制val test = Text()
val str = test.str // 等价于 val str = test.delegate.getValue(test, kProperty)
val test.str = str // 等价于 test.delegate.setValue(test, Kproperty, str)
委托应用
更简便地获取传参
委托可以隐藏细节,特别是当细节是一些模板代码的时候:
代码语言:javascript复制class TestFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val id = arguments?.getString("id") ?: ""
}
}
class KotlinActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val id = intent?.getStringExtra("id") ?: ""
}
}
获取传递给 Activity 或 Fragment 值的代码就很模板。可以使用委托隐藏一下细节:
代码语言:javascript复制// 新建 Extras 类作为被委托类
class Extras<out T>(private val key: String, private val default: T) {
// 重载取值操作符
operator fun getValue(thisRef: Any, kProperty: KProperty<*>): T? =
when (thisRef) {
// 获取传递给 Activity 的参数
is Activity -> { thisRef.intent?.extras?.get(key) as? T ?: default }
// 获取传递给 Fragment 的参数
is Fragment -> { thisRef.arguments?.get(key) as? T ?: default }
else -> default
}
}
然后就可以像这样使用委托:
代码语言:javascript复制class TestActivity : AppCompatActivity() {
private val id by Extras("id","0")
}
class TestFragment : Fragment() {
private val id by Extras("id","0")
}
更简便地获取 map 值
有些类的属性不是固定的,而是有时多,有时少,即动态的,比如:
代码语言:javascript复制class Person {
private val attrs = hashMapOf<String, Any>()
fun setAttrs( key: String, value: Any){
attrs[key] = value
}
val name: String
get() = attrs["name"]
}
有些Person有孩子,有些没有,所以不同Person实例拥有的属性集是不同的。这种场景用Map来存储属性就很合适。
上述代码可以用委托简化:
代码语言:javascript复制class Person {
private val attrs = hashMapOf<String, Any>()
fun setAttrs( key: String, value: Any){
attrs[key] = value
}
val name: String by attrs
}
将name的获取委托给一个 map 对象。神奇之处在于,甚至都不需要指定key就可以正确地从 map 中获取 name 属性值。这是因为 Kotlin 标准库已经为 Map 定义了getValue()和setValue()扩展函数。属性名将自动作用于 map 的键。
总结
- Kotlin 委托分为类委托和属性委托。它们都通过关键词by来进行委托。
- 类委托可以用简洁的语法将类的实现委托给另一个对象,以减少模板代码。
- 属性委托可以将对属性的访问委托给另一个对象,以减少模板代码并隐藏访问细节。
- 属性委托有三种实现方式,分别是扩展方法、实现ReadWriteProperty接口、重载运算符。