12

Kotlin修炼指南(五)—Delegates

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzAxNzMxNzk5OQ%3D%3D&%3Bmid=2649487116&%3Bidx=1&%3Bsn=9c93c338206de5056170038b565b4d88
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

委托,是一种比较常见的设计模式,通常采用接口或者抽象类的方式来实现,在Java代码中,一般使用接口来进行封装,而在kotlin中,可以通过委托机制来实现更加方便的委托模式。

Kotlin中的委托分为两种——类委托与属性委托,其中属性委托,是Kotlin非常强大的一个语法糖,借助这个功能,我们可以消除很多重复的模板代码,将Kotlin的代码榨干到极致。

类委托

下面我们先通过一个简单的例子来了解下什么是类委托,以及类委托的具体作用。

类委托入门

在一般的业务开发中,我们经常会遇到这样的场景——一个业务功能,有多种实现,通过接口来封装具体的业务方法,通过实现接口来完成不同实现,这样的场景有很多,使用Kotlin来实现这一功能,步骤如下。

第一步:创建接口约束,抽象业务场景。例如下面这个数据持久化的例子,我们通过接口定义了三个数据操作方法。

interface IDataPersistence {
fun addData()
fun delData()
fun queryData()
}

第二步:创建委托的实现,实现约束接口。数据持久化有多种不同的实现方式,下面这就是简单的两种,一种是通过SQL进行持久化,另一种是通过SharedPreferences进行持久化。

class SQL : IDataPersistence {
override fun addData() {
Log.d("xys", "addData with SQL")
}

override fun delData() {
Log.d("xys", "delData with SQL")
}

override fun queryData() {
Log.d("xys", "queryData with SQL")
}
}

class SharedPreferences : IDataPersistence {
override fun addData() {
Log.d("xys", "addData with SharedPreferences")
}

override fun delData() {
Log.d("xys", "delData with SharedPreferences")
}

override fun queryData() {
Log.d("xys", "queryData with SharedPreferences")
}
}

第三步:调用约束接口,即业务方调用,但不用考虑具体的实现。类委托的语法格式是,<类>:<约束接口> by <实现类的实例>,即通过by关键字,将接口的实现,委托给一个具体的实例来作为自己的实现。

class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate

使用方式与Java代码通过接口来实现基本一致,即在类初始化的时候,传入具体的实现类即可。

// val myDB = MyDB(SQL())
val myDB = MyDB(SharedPreferences())
myDB.addData()
myDB.delData()
myDB.queryData()

在Kotlin的类委托机制中,调用方和业务实现方,都需要实现约束接口,调用方只需要传入不同类型的业务实现方式,即可通过约束调用具体的实现。这一点看上去好像并没有比Java方便多少,但是在Kotlin中,在某些简单的场景下,实际上是可以省略掉实现类的,直接通过对委托实现的重写来实现委托接口,代码如下所示。

class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate {
override fun addData() {}

override fun delData() {}

override fun queryData() {}
}

再简单一点,如果你不用传入多种不同的实例,可以在构造方法中去掉默认参数,直接在by关键字后面添加具体的接口实现,还是上面的例子,代码如下所示。

class MyDB : IDataPersistence by SQL()

调用:
MyDB().addData()

通过委托,可以在不影响继承(MyDB可以继承其它类)的情况下,通过委托,使用指定接口中的方法。

类委托的原理

通过反编译Kotlin实现的代码,可以很方便的了解Kotlin内部是如何通过Java代码来实现委托机制的。

jAFRr2Y.png!mobile

实际上就是在调用者内部创建一个实现者的变量,在实现的接口方法中,变量调用该方法,从而实现调用,一切都只是语法糖而已,Kotlin帮你简化了代码。

类委托的使用场景

通过类委托机制,可以很方便的实现多态。这是类委托最重要的使用场景,通过接口定义来实现多态性,同时使用by关键字来简化Java中接口实现的冗余代码,下面的这个简单的例子,就是一个最好的说明。

class RedSquare : Shape by Square(), Color by Red() {
fun draw() {
print("draw Square with Red")
}
}

另外,委托还可以用于在不修改原来代码及架构的基础上,对原有功能扩展或者修改。例如我们要对MutableList类拓展一个函数,如果是Java代码,或者不使用委托的Kotlin代码,你必须实现List接口中的所有函数,虽然你未作修改,只是单纯的传递调用,但是需要为这个拓展写很多无用的代码,而使用委托,则完全不用处理这些冗余,代码如下所示。

class NewList(private val list: MutableList<String>) : MutableList<String> by list {
fun newFunction() {}
}

Kotlin会自动在编译时帮你添加其它接口方法的默认实现。

属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性统一管理,属性委托的一般格式如下所示。

val/var <属性名>: <类型> by <表达式>

在前面的讲解中,类委托,委托的是接口中指定的方法,而属性委托,则委托的是属性的get、set方法,属性委托实际上就是将get、set方法的逻辑委托给一个单独的类来进行实现(对于val属性来说,委托的是getValue方法,对于var属性来说,委托的是setValue和getValue方法)。

属性委托在那些需要对属性的get、set方法复用逻辑的场景下,是非常方便的,下面通过一个简单的例子来演示下属性委托机制。

首先,我们定义一个var属性,并将其委托给MyDelegate类,即将get和set方法进行了交接托管,因此,MyDelegate类需要重写getValue和setValue方法,为其提供新的返回值和逻辑,代码如下所示。

var delegateProp by MyDelegate()

class MyDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "MyDelegate get $thisRef ${property.name}"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
Log.d("xys", "MyDelegate set $value $thisRef ${property.name}")
}
}

调用:
Log.d("xys", delegateProp)
delegateProp = "abc"

out:
com.yw.demo D/xys: MyDelegate get com.yw.demo.MainActivity@595c528 delegateProp
com.yw.demo D/xys: MyDelegate set abc com.yw.demo.MainActivity@595c528 delegateProp

这样处理之后,我们在使用delegateProp这个属性的时候,就会自动拓展MyDelegate中的处理。

不过呢,这样写起来太麻烦,MyDelegate中的方法都需要手动来实现,所以Kotlin提供了两个接口来帮助开发者实现。

JFzquey.png!mobile

所以上面的代码可以简写成下面这样。

class MyDelegate : ReadWriteProperty<Any, String> {
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
Log.d("xys", "MyDelegate set $value $thisRef ${property.name}")
}

override fun getValue(thisRef: Any, property: KProperty<*>): String {
return "MyDelegate get $thisRef ${property.name}"
}
}

属性委托使用场景

那么这东西有什么用呢,下面举个例子。

逻辑封装

例如对参数进行encode的操作。

object Prop {
var encodeProp: String by EncodeProperty("init")
}

class EncodeProperty(var value: String) : ReadWriteProperty<Prop, String> {
override fun getValue(thisRef: Prop, property: KProperty<*>): String {
return "get encode prop output $value"
}

override fun setValue(thisRef: Prop, property: KProperty<*>, value: String) {
this.value = value
Log.d("xys", "save encode prop $value")
}
}

调用:
Prop.encodeProp = "xuyisheng"
Log.d("xys", Prop.encodeProp)

参数依然是那个参数变量,但是对它的处理被外包出去,交给了EncodeProperty来进行处理,这里的实现就是业务需要的encode操作,将来如果encode操作有改动,那么只需要修改EncodeProperty即可。也就是说,我们将encode的具体逻辑进行了封装,这样便于拓展和维护。

消除模板代码

再来看下面这个例子,Person类中有两个属性,分别修改了set方法,用于添加一些逻辑,代码如下所示。

class Person {
var firstName: String = ""
set(value) {
field = value.toLowerCase()
}
var lastname: String = ""
set(value) {
field = value.toLowerCase()
}
}

调用:
val person = Person()
person.firstName = "XU"
person.lastname = "YISHENG"
println("${person.firstName} ${person.lastname}")

但是这里的两个属性的set方法,要处理的逻辑基本是一样的,即对字母做小写,所以我们对这个操作进行抽取,设置一个委托,代码如下所示。

class FormatDelegate : ReadWriteProperty<Any?, String> {
private var formattedString: String = ""

override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return formattedString
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
formattedString = value.toLowerCase()
}
}

这个委托做的事情,和在前面的代码中set的逻辑是一样的。那么这个时候,就可以对Person类进行改造了,代码如下所示。

class Person {
var firstName: String by FormatDelegate()
var lastname: String by FormatDelegate()
}

这样就将同样的set操作的逻辑,封装在了FormatDelegate中,从而实现了模板代码的消除。

抽象属性委托的一般步骤

从上面的例子我们可以发现,其实只要是对属性的get、set方法有操作的地方,几乎都可以使用属性委托来简化,对于这种操作,开发者一般会经历下面几个过程。

  • 青铜:抽取公共函数,在处理时对属性进行调用

  • 黄金:重新属性的get、set函数,将逻辑封装

  • 王者:使用属性委托,将逻辑抽取出来

下面再通过一个实例,来演示下这个步骤。我们以Fragment的启动方式为例来讲解,经常有写类似的代码来处理Fragment的启动。

const val PARAM1 = "param1"
const val PARAM2 = "param2"

class DemoFragment : Fragment() {
private var param1: Int? = null
private var param2: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
param1 = args.getInt(PARAM1)
param2 = args.getString(PARAM2)
}
}

companion object {
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
arguments = Bundle().apply {
putInt(PARAM1, param1)
putString(PARAM2, param2)
}
}
}
}

首先,我们可以通过Kotlin的set、get函数进行改写,将arguments的填充,放到属性的get、set函数内部,代码如下所示。

class DemoFragment : Fragment() {
private var param1: Int?
get() = arguments?.getInt(PARAM1)
set(value) {
value?.let {
arguments?.putInt(PARAM1, it)
}
}

private var param2: String?
get() = arguments?.getString(PARAM2)
set(value) {
arguments?.putString(PARAM2, value)
}

companion object {
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
this.param1 = param1
this.param2 = param2
}
}
}

但是我们还是要为每个属性写重复的代码,特别是当属性很多的时候,每个属性都要写重复的put、get函数,所以,下面使用委托对这个逻辑再进行一次封装,代码如下所示。

class DemoFragment : Fragment() {
private var param1: Int by FragmentArgumentDelegate()

private var param2: String by FragmentArgumentDelegate()

companion object {
fun newInstance(param1: Int, param2: String): DemoFragment =
DemoFragment().apply {
this.param1 = param1
this.param2 = param2
}
}
}

@Suppress("UNCHECKED_CAST")
class FragmentArgumentDelegate<T : Any> : ReadWriteProperty<Fragment, T> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val key = property.name
return thisRef.arguments?.get(key) as T
}

override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
val arguments = thisRef.arguments
val key = property.name
arguments?.put(key, value)
}
}

fun <T> Bundle.put(key: String, value: T) {
when (value) {
is Boolean -> putBoolean(key, value)
is String -> putString(key, value)
is Int -> putInt(key, value)
is Short -> putShort(key, value)
is Long -> putLong(key, value)
is Byte -> putByte(key, value)
is ByteArray -> putByteArray(key, value)
is Char -> putChar(key, value)
is CharArray -> putCharArray(key, value)
is CharSequence -> putCharSequence(key, value)
is Float -> putFloat(key, value)
is Bundle -> putBundle(key, value)
is Parcelable -> putParcelable(key, value)
is Serializable -> putSerializable(key, value)
else -> throw IllegalStateException("Type of property $key is not supported")
}
}

这里要注意的是,Bundle没有提供单个属性的put拓展,所以我们需要自己实现一个。

通过上面的这些操作,就将Fragment参数传递的代码简化到了只有一行,其它任何的Fragment传参,都可以使用这个委托。

委托操作实例

最后,再介绍一些官方推荐的委托使用场景。

内置委托函数

Kotlin系统库提供了很多有用的委托,这些都内置在Delegate库中。

延迟属性lazy

属性委托,可以是一个表达式,借助这个特性,可以实现属性的延迟加载,即在第一次访问的时候进行初始化。

private val lazyProp: String by lazy {
Log.d("xys", "表达式只会执行一次")
"执行后赋值给lazyProp"
}

Log.d("xys", lazyProp)
Log.d("xys", lazyProp)

out:
D/xys: 表达式只会执行一次
D/xys: 执行后赋值给lazyProp
D/xys: 执行后赋值给lazyProp

要注意的是,lazy表达式中的代码,只会在第一次初始化的时候调用,之后就不会调用了,所以这里log只打印了一次。

观察属性observable

Delegates.observable可以非常方便的帮助我们实现观察者模式,代码如下所示。

var observableProp: String by Delegates.observable("init value 0") { property, oldValue, newValue ->
Log.d("xys", "change: $property: $oldValue -> $newValue ")
}

Log.d("xys", observableProp)
observableProp = "change value"

当属性值发生改变的时候,就会通知出来。

借助观察属性,可以很方便的实现时间差的判断,例如连续back退出的功能,代码如下所示。

private var backPressedTime by Delegates.observable(0L) { pre, old, new ->
if (new - old < 2000) {
finish()
} else {
Toast.makeText(this, "再按一次返回退出", Toast.LENGTH_SHORT).show()
}
}

override fun onBackPressed() {
backPressedTime = System.currentTimeMillis()
}

条件观察属性vetoable

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效,代码如下所示。

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

SharedPreferences操作简化

前面我们提到了,只要是涉及到get、set方法的使用的场景,几乎都可以使用委托来进行优化,再拓展一下,凡是对属性有进行读写操作的,都可以使用委托来进行优化,例如我们在Android中比较常用的SharedPreferences操作,大部分情况下,都会抽取工具类,类似下面这样进行调用。

PreferencesUtil.getInstance().putBoolean(XXXXX, false);

下面通过委托,我们可以将一个普通属性的读写进行代理,代理到通过SP读写,这样我们在代码中对这个属性的读写,实际上是将其代理到SP中,代码如下所示。

@Suppress("UNCHECKED_CAST")
class PreferenceDelegate<T>(private val context: Context, private val propName: String, private val defaultValue: T) : ReadWriteProperty<Any, T> {

private val sharedPreferences: SharedPreferences by lazy { context.getSharedPreferences("SP_NAME", Context.MODE_PRIVATE) }

override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
value?.let { putSPValue(propName, value) }
}

override fun getValue(thisRef: Any, property: KProperty<*>): T {
return getSPValue(propName, defaultValue) ?: defaultValue
}

private fun <T> getSPValue(name: String, defaultValue: T): T? = with(sharedPreferences) {
val result = when (defaultValue) {
is String -> getString(name, defaultValue)
is Int -> getInt(name, defaultValue)
is Long -> getLong(name, defaultValue)
is Float -> getFloat(name, defaultValue)
is Boolean -> getBoolean(name, defaultValue)
else -> null
}
result as T
}

private fun <T> putSPValue(name: String, value: T) = with(sharedPreferences.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 -> null
}
}?.apply()
}

使用:
var valueInSP: String by PreferenceDelegate(this, "test", "init")

Log.d("xys", valueInSP)
valueInSP = "new value"
Log.d("xys", valueInSP)

out:
D/xys: init
D/xys: new value

通过上面的操作,我们在使用SharedPreferences的时候,只需要对某个要操作的属性使用by进行标记,将其委托给PreferenceDelegate即可,这样表面上好像是在操作一个String,但实际上,已经是对SharedPreferences的操作了。

在下面这个lib中,对很多场景下的委托进行了封装,大家可以参考下它的实现。

https://github.com/fengzhizi715/SAF-Object-Delegate

向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK