理解委托
委托是一种设计模式,具体的操作不用自己实现,而是把操作委托给另一个辅助的对象,我们把这个辅助对象称为委托。
注:本篇博客内容来自《Kotlin实战》一书,经过自己的消化与学习整理的。
类委托
类委托,委托和代理似乎是在讲同一个东西,比如,一个老板想喝咖啡,老板嘛,比较懒,不想自己去买,于是他叫经理给他买,经理又叫他的一个手下去买。在这个关系中,经理可以理解为代理,代老板买东西,但是经理也没有直接去买,而是把工作交给了他的手下去买,这就可以理解成委托,委托他的手下去买。
我们可以这样理解,经理是一个代理,他可以做很多的事情,但是真正做事情的时候是委托给他的手去做的。
在代码中写一个示例,我们写一个集合的代理类,这个代理可以完成集合的各种操作,但是它是委托给ArrayList来完成的,如下:
class DelegatingCollection<T> : Collection<T> {
private val innerList = ArrayList<T>()
override val size: Int get() = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun iterator(): Iterator<T> = innerList.iterator()
}
如上代码,DelegatingCollection是一个集合代理类,它的功能和ArrayList一模一样,因为它的功能都是委托ArrayList来完成的,那我们为什么不直接使用ArrayList而要使用代理类呢?使用代理类我们是可以做一些其他事情的,比如在代理中打印代理函数的执行时间、调用次数等。
写一个代理类还是比较容易的,但是如果方法很多,这样写起来也是有点烦人的,通过Kotlin的类委托就可以解决这个烦恼,如下:
class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList
Ok,就是这么清爽,这就像Kotlin中的data class一样,自动为你生成需要的方法。
我们也可以覆盖掉某些方法,自己实现,如下:
class DelegatingCollection<T>(val innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList {
private var invokeCount = 0
override fun isEmpty(): Boolean {
invokeCount++
return innerList.isEmpty()
}
}
如上代码,我们覆盖了isEmpty()函数,在其中计算该函数被调用的次数,虽然最后也是委拖给innerList来完成isEmpty的判断,但是在其他场合,你完全可以使用自己的实现而不进行委托,比如经理,他的很多工作是委托给他的手下去完成的,但是有一些工作他不想委托给他的手,那他就自己实现了罗,比如泡妞自己实现就比较好_。
属性委托
属性委托是把一个属性的访问器(即setter和getter)的逻辑委托给一个辅助对象。示例如下:
class Foo {
var p: Type
}
如上代码,这是一个简单的kotlin类,有一个属性p,我们以可以自定义该属性的setter和getter,如下:
class Foo {
var p: Type
set(value: Type) { ... }
get() = ...
}
如上代码完成了setter和getter的自定义,这些都是kotlin基础就有讲的。然后,我们可以把setter和getter的工作交给一个代理类来完成,如下:
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
如上代码,调用属性p的setter和getter的具体实现逻辑交给delegate的setVaule和getValue来完成(setVaule仅适用于可变属性),setValue和getValue可以是成员函数,也可以是扩展函数。Delegate的简单实现如下:
class Delegate {
operator fun getValue(...) { ... }
operator fun setValue(..., value: Type) { ... }
}
按照Kotlin的约定,属性的委托类必须要有getValue和setValue方法(setVaule仅适用于可变属性),方法的具体参数后面会讲解。只要符合这个约定,我们就可以使用Kotlin的简洁语法完成属性委托,如下:
class Foo {
var p: Type by Delegate
}
fun main() {
val foo = Foo()
val oldValue = foo.p
foo.p = newValue
}
使用属性委托来实现惰性初始化
惰性初始化是一种常见的模式,比如在设计单例的时候可以设计为懒加载的单例,即单例的初始化是惰性初始化的,用到的时候才把单例实例化,没有到的时候为null。
示例:一个Person有emails属性,保存了此人的所有邮件,当我们不访问这个属性的时候,它为null,当我们访问它的时候,如果为null就进行加载emails的操作(耗时操作),加载完成后emails就不为空了,当再次访问emails属性时,它就不会再执行加载emails的操作了,因为之前已经加载好了,这就是惰性初始化,不需要的时候为null,需要的时候才初始化,而且只初始化一次。示例代码如下:
class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails
}
}
这里使用了所谓的支持属性技术:即一个属性,用两个字段来完成,一个是_emails,用来存储这个属性的值,另一个是emails,用来提供对属性的读取。你需要两个属性来完成,因为他们具有不同的类型:_emails可以为空,而emails为非空,这种技术经常会使用到,值得熟练掌握。
但这个代码有点繁瑣,如果需要多个这样的惰性属性,那这个类就很臃肿了。而且,它并不总是正常运行:这个实现不是线程安全的,Kotlin提供了更好的解决方案,使用委托属性会让代码变得简单得多,通过lazy函数来返回委托的属性,lazy是一个标准的库函数,示例如下:
class Person(val name: String) {
val emails by lazy{ loadEmails(this) }
}
lazy函数返回一个对象,该对象具有一个名为getValue且签名正确的方法,因此可以把它与by关键字一起使用来创建一个委托属性。lazy函数的参数是一个lambda,可以调用它来初始化这个值。默认情况下,lazy函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用哪个锁,或者完全避开同步,如果该类永远不会在多线程环境中使用。
要了解委托属性的实现方式,让我们来看另一个例子:当一个对象的属性更改时通知监听器,在这许多不同的情况下都很有用,例如:当对象显示在UI时,你希望在对象变化时UI能自动刷新。Java具有用于此类通知的标准机制:PropertyChangeSupport和PropertyChangeEvent类。让我们看看在Kotlin中不使用委托属性的情况下,该如何使用它们,然后我们再将代码重构为用委托属性的方式。
PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,你通常需要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。
为了避免要在每个类中去添加这个字段,你需要创建一个小的工作类,用来存储PropertyChangeSupport的实例并监听属性更改。之后,你的类会继承这个工具类,以访问changeSupport,如下:
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
现在我们来写一个Person类,定义一个只读属性(作为一个人的名称,一般不会随时更改)和两个可写属性:年龄和工资,当这个人的年龄或工资发生变化 时,这个类将通知它的监听器。代码如下:
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary", oldValue, newValue)
}
}
fun main() {
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener(
PropertyChangeListener { event ->
println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
}
)
p.age = 35
p.salary = 2100
}
setter中有很多重复的代码,我们把它提取到一个类,如下:
class ObservableProperty(
val propName: String,
var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(newValue) = _age.setValue(newValue)
val _salary = ObservableProperty("salary", age, changeSupport)
var salary: Int
get() = _salary.getValue()
set(newValue) = _salary.setValue(newValue)
}
fun main() {
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener(
PropertyChangeListener { event ->
println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
}
)
p.age = 35
p.salary = 2100
}
现在,你应该已经差不多理解了在Kotlin中,委托属性是如何工作的。你创建了一个保存属性值的类,并在修改属性时自动触发更改通知。你删除了重复的逻辑代码,但是需要相当多的样板代码来为每个属性创建ObservableProperty实例,并把getter和setter委托给它。Kotlin的委托属性功能可以让你摆脱这些样板代码。但是在此之前,你需要更改ObservableProperty方法的签名,以符合Kotlin的约定方法。
class ObservableProperty(
var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
与之前的版本相比,这次代码做了一些更改:
- 现在,按照约定的需要,getValue和setValue函数被标志了operator。
- 这些函数加了两个参数:一个用于接收属性的实现,用来设置或读取属性,另一个用于表示 属性本身。这个属性类型为KProperty。
- 把name属性从主构造方法中删除了,因为现在可以通过KProperty访问属性名称了。
终于,你可以见识Kotlin委托属性的神奇了,如下:
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}
通过关键字by,Kotlin编译器会自动执行之前版本的代码中手动完成的操作。如果把这份代码与以前版本的Person类进行比较:使用委托属性时生成的代码非常类型。右边的对象被称为委托。Kotlin会自动将委托存储在隐藏的属性中,并在访问或修改属性时调用委托的getValue和setValue。
你不用手动去实现可观察的属性逻辑,可以使用Kotlin标准库,它已经包含了类似于ObservableProperty的类。标准库和这里使用的PropertyChangeSupport类没有耦合,因此你需要传递一个lambda,来告诉它如何通知属性值的更改。可以这样做:
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
private val observer = { prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
by右边的表达式不一定是新创建的实例,也可以是函数调用、另一个属性或任何其他表达式,只要这个表达式的值能够被编译器用正确的参数类型来调用getValue和setValue的对象。与其他约定一样,getValue和setValue可以是对象自己声明的方法或扩展函数。
注意:为了让示例保持简单,我们只展示了如何使用类型为Int的委托属性,委托属性机制其实是通用的,适用于任何其他类型。
委托属性的变换规则
让我们来总结一下委托属性是怎么荼的,假设你已经有了一个具有委托属性的类:
class C {
var prop: Type by MyDelegate()
}
val c = C()
MyDelegate实例会被保存到一个隐藏的属性中,它被称为:<delegate>,编译器也将用一个KProperty类型的对象来代表这个属性,它被称为:<property>。编译器生成的代码如下:
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
因为,在每个属性访问器中,编译器都会生成对应的getValue和setValue方法,如下:
val x = c.prop => val x = <delegate>.getValue(c, <property>)
c.prop = x => <delegate>.setValue(c, <property>, x)
这个机制非常简单,但它可以实现许多有趣的场景。你可以自定义存储该属性值的位置(map、数据库表或者用户会话的Cookie中),以及在访问该属性时做点什么(比如添加验证、更改通知等)。所有这一切都可以用紧凑的代码完成。我们再来看看标准库中委托属性的另一个用法,然后看看如何在自己的框架中使用它们。
在map中保存属性值 委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(expando)对象。例如,考虑一个联系人管理系统,可以用来存储有关联系人的任意信息。系统 中的每个人都有一些属性需要特殊处理(例如名字),以及每个人特有的数量任意的额外属性(例如,最小的孩子的生日)。
实现这种系统的一种方法是将人的所有属性存储在map中,不确定提供属性,来访问需要特殊处理的信息。来看个例子:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
fun main() {
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data) {
p.setAttribute(attrName, value)
}
println(p.name)
}
这里使用了一个通用的API来把数据加载到对象中(在实际项目中,可以是JSON反序列化或类似的方法),然后使用特定的API来访问一个属性的值。把它改为委托属性非常简单,可以直接将map放在by关键字后面,如下:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,所以这里可以直接这样用。属性的名称将自动用作在map中的键,属性值作为map中的值。
框架中的委托属性
更改存储和修改属性的方式对框架的开发人员非常有用。假设数据库中Users的表包含两列数据:字符串类型的name和整形的age。可以在Kotlin中定义Users和User类。在Kotlin代码中,所有存储在数据库中的用户实体的加载和更改都可以通过User类的实例来操作。
object Users : IdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users对象描述数据库表的一个表:它被声明为一个对象,因为它对应整个表,所以只需要一个实例。对象的属性表示数据表的列。
User类的基类Entity,包含了实体的数据库列与值的映射。特定User的属性拥有这个用户在数据库中指定的值name和age。
框架用起来会特别方便,因为访问属性会自动从Entity类的映射中检索相应的值,而修改过的对象会被标记成脏数据,在需要时可将其保存到数据库中。可以在Kotlin代码中编写user.age += 1,数据库中的相应实体将自动更新。
现在你已经充分了解了如何实现具有这种API的框架。每个实体属性(name, age)都实现为委托属性,使用对象(Users.name, Users.age)作为委托:
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
让我们来看看怎样显式地指定列的类型:
object Users : IdTable() {
val name: Column<String> = varchar("name", length = 50).index()
val age: Column<Int> = integer("age")
}
至于Column类,框架已经定义了getValue和setValue方法,满足Kotlin的委托约定:
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T): T {
}
可以使用Column属性(Users.name)作为被委托属性(name)的委托。当在代码中写入user.age += 1时,代码的执行将类似于user.ageDelegate.setValue(user.ageDelegate.getValue() + 1) (省略了属性和对象实例的参数)的操作。getValue和setValue方法负债检索和更新数据库中的信息。
总结:
- 委托属性可以用来重用逻辑,这些逻辑控制如何存储、初始化、访问和修改属性值,这是用来构建框架的一个强大的工具。
- lazy标准库函数提供了一种实现惰性初始化属性的简单方法
- Delegate.observable函数可以用来添加属性更改的观察者。
- 委托属性可以使用任意map来作为属性委托,来灵活来处理具有可变属性集的对象。
|