委托模式被证实是一种很好的替代继承的方式,Kotlin 在语言层面对委托模式提供了很是优雅的支持(语法糖)。html
先给你们看看我用 Kotlin 的属性委托语法糖在 Android 工程里面作的一件有用工做——SharedPreferences
的读写委托。java
文中陈列的全部代码已汇总成 Demo 传至 github,点这儿获取源码。建议配合 Demo 阅读本文。git
项目主要文件结构以下:github
│ App.kt
│
├─base
│ SpBase.kt
│
├─delegates
│ SPDelegates.kt
│ SPUtils.kt
│
├─demo
└─ui
MainActivity.kt
复制代码
先来看看 delegates 包下的文件。编程
SPUtils
是个读写 SharedPreferences
(如下简称 SP) 存储项的基础工具类:设计模式
/** * @author xiaofei_dev * @desc 读写 SP 存储项的基础工具类 */
object SPUtils {
val SP by lazy {
App.instance.getSharedPreferences("default", Context.MODE_PRIVATE)
}
//读 SP 存储项
fun <T> getValue(name: String, default: T): T = with(SP) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: ""
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw java.lang.IllegalArgumentException()
}
@Suppress("UNCHECKED_CAST")
res as T
}
//写 SP 存储项
fun <T> putValue(name: String, value: T) = with(SP.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
复制代码
主要使用泛型实现了完善的 SP 读写,总体仍是很是简洁易懂的。上下文对象使用了自定义的 Application
类实例(见 Demo 中的 App 类)。安全
下面重点来看一下 SPDelegates
类的定义:app
/** * @author xiaofei_dev * @desc <p>读写 SP 存储项的轻量级委托类,以下, * 读 SP 的操做委托给该类对象的 getValue 方法, * 写 SP 操做委托给该类对象的 setValue 方法, * 注意这两个方法不用你显式调用,把一切交给编译器就行(仍是语法糖) * 具体使用此类定义 SP 存储项的代码请参考 SpBase 文件</p> */
class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return SPUtils.getValue(key, default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
SPUtils.putValue(key, value)
}
}
复制代码
SPDelegates
类实现了 Kotlin 标准库中声明的用于属性委托的 ReadWriteProperty
接口(SPDelegates
类的用法后面会详细说到),从名字能够看出此接口是可读写的(适用于 var
声明的属性),除此以外还有个 ReadOnlyProperty
(只读)接口(适用于 val
声明的属性)。ide
对于属性的委托类(以SPDelegates
为例),要求必须提供一个 getValue()
函数(和一个setValue()
函数——对于 var 属性)。其getValue
方法的参数要求以下:函数
thisRef
—— 必须与 属性所属类 的类型(对于扩展属性——指被扩展的类型)相同或是它的超类型(参见后面 SpBase 单例类中的注释);property
—— 必须是类型 KProperty<*>
(Kotlin 标准库 kotlin.reflect (反射)包下的一个类)或其超类型。对于其 setValue
方法,前两个参数同 getValue
。第三个参数value
必须与属性同类型或是它的子类型。
以上概念暂时看不懂没关系,下面经过委托属性的具体应用来加深理解。
接着是具体使用到委托属性的 SpBase
单例类:
/** * @author xiaofei_dev * @desc 定义的 SP 存储项 */
object SpBase{
//SP 存储项的键
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
// 这就定义了一个 SP 存储项
// 把 SP 的读写操做委托给 SPDelegates 类的一个实例(使用 by 关键字,by 是 Kotlin 语言层面的一个原语),
// 此时访问 SpBase 的 contentSomething (你能够简单把其当作 Java 里的一个静态变量)属性便是在读取 SP 的存储项,
// 给 contentSomething 属性赋值便是写 SP 的操做,就这么简单
// 这里用到的 SPDelegates 对象的 getValue 方法的 thisRef(见上文) 参数的类型正是外层的 SpBase
var contentSomething: String by SPDelegates(CONTENT_SOMETHING, "我是一个 SP 存储项,点击编辑我")
}
复制代码
上面代码中,单例 SpBase
的属性 contentSomething
就是一个定义好的 SP 存储项。得益于语言级别的强大语法糖支持,写出来的代码能够如此简洁而优雅。读写 SP 存储项的请求经过属性委托给了一个 SPDelegates
对象,委托属性的语法为
val/var <属性名>: <类型> by <表达式>
其最后会被编译器解释成下面这样的代码(大体上):
object SpBase{
private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
private val propDelegate = SPDelegates(CONTENT_SOMETHING, "我是一个 SP 存储项,点击编辑我")
var contentSomething: String
get() = propDelegate.getValue(this, this::contentSomething)//读SP
set(value) = propDelegate.setValue(this, this::contentSomething, value)//写SP
}
复制代码
仍是比较容易理解的。下面演示下这个定义好的 SP 存储项如何使用,见 Demo 的 MainActivity 类文件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
}
private fun initView(){
//读取 SP 内容显示到界面上
editContent.setText(SpBase.contentSomething)
btnSave.setOnClickListener {
//保存 SP 项
SpBase.contentSomething = "${editContent.text}"
Toast.makeText(this, R.string.main_save_success, Toast.LENGTH_SHORT).show()
}
}
}
复制代码
总体比较简单,就是个读写 SP 存储项的过程。你们能够实际运行下 Demo 看下具体效果。
上文述及的 SPDelegates
类实现了 Kotlin 标准库提供的 ReadWriteProperty
接口,咱们固然也能够不借助任何接口来实现一个属性委托类,只要其提供一个getValue()
函数(和一个setValue()
函数——对于 var 属性)而且符合咱们上面讨论的参数要求就行。下面来定义一个平凡的属性委托类 Delegate
(见 Demo 的 demo 包下 Example 文件):
/** * @author xiaofei_dev * @desc 不用实现任何接口的平凡属性委托类 */
class Delegate<T> {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
println("$thisRef, thank you for delegating '${property.name}' to me! The value is $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
this.value = value
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
复制代码
使用方式依旧:
class Example {
//委托属性
var p: String? by Delegate()
}
fun main(args: Array<String>) {
val e = Example()
e.p = "hehe"
println(e.p)
}
复制代码
控制台输出以下:
hehe has been assigned to 'p' in com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb.
com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb, thank you for delegating 'p' to me! The value is hehe
hehe
复制代码
你能够本身跑下试试~
有必要单独花篇幅解释下何为委托模式。
简而言之,在委托模式中,有两个对象共同处理同一个请求,接受请求的对象将请求委托给另外一个对象来处理。
委托模式最简单的例子:
//委托类,墨水能用来打印文字( ̄▽ ̄)"
class Ink {
fun print() {
print("This message comes from the delegate class,Not Printer.")
}
}
class Printer {
//委托对象
var ink = Ink()
fun print() {
//Printer 的实例会将请求委托给另外一个对象(DelegateNormal 的对象)来处理
ink.print()//调用委托对象的方法
}
}
fun main(args: Array<String>) {
val printer = Printer()
printer.print()
}
复制代码
控制台输出以下:
This message comes from the delegate class,Not Printer.
复制代码
委托模式使咱们能够用聚合来代替继承,是许多其余设计模式(如状态模式、策略模式、访问者模式)的基础。
Kotlin 能够作到零样板代码实现委托模式(而不是像上面展现的那样还须要样板代码)!
好比咱们如今有以下接口和类:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
复制代码
Base
接口想作的就是在控制台打印些什么东西。这没啥问题,咱们已经在 BaseImpl
类上完整实现了 Base
接口。
此时咱们想再给 Base 接口写一个实现时能够这么作:
class Derived(b: Base) : Base by b
复制代码
这其实跟下面的写法是等价的(编译器实际生成的):
class Derived(val delegate: Base) : Base {
override fun print() {
delegate.print()
}
}
复制代码
注意不是下面这种:
class Derived(val delegate: Base){
fun print() {
delegate.print()
}
}
复制代码
Kotlin 经过编译器的黑魔法将许多样板代码封印在了 by
这样一个语言级别的原语中(又是语法糖)。使用方式:
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print()
}
复制代码
控制台输出以下:
10
复制代码
说回属性委托,Kotlin 的标准库为一些经常使用的委托写好了工厂方法,下面一一列举。
fun main(args: Array<String>) {
//延迟计算属性的值,lazy 后面 lambda 表达式中的逻辑只会执行一次(且是线程安全的)并记录结果,后续调用属性的 get() 方法只是返回记录的结果
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
println(lazyValue)
println(lazyValue)
}
复制代码
控制台输出以下:
computed!
Hello
Hello
复制代码
Delegates.observable()
接受两个参数:初始值与修改时处理程序。 每次给属性赋值时就会调用该处理程序(在赋值后执行)。处理程序有三个参数:被赋值属性的 KProperty
对象、旧值与新值:
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
复制代码
控制台输出以下:
<no name> -> first
first -> second
复制代码
你甚至能够在一个映射(map
)中存储属性的值。 这种状况下,你能够直接将属性委托给映射实例:
class Student(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main(args: Array<String>) {
val student = Student(mapOf(
"name" to "xiaofei",
"age" to 25
))
println(student.name)
println(student.age)
}
复制代码
固然这种应用必须确保属性的名字和 map
中的键对应起来,否则你可能会收获一个 NoSuchElementException
运行时异常,大概像这样:
java.util.NoSuchElementException: Key XXXX is missing in the map.
复制代码
言止于此,未完待续。