写这篇文章的原因
发现组内很多写kotlin的人对by 这个关键字 都不太会用,最多也就是 by lazy 来findview一下。 但是网上一搜 关于by的文章 很多,我发现这些文章都有一个毛病 就是只讲了 这个by的 语法 对应的api 怎么用。 却从来没有一个人讲清楚 为什么要用by? 什么时候用by? 其实这些问题的根源就是 很多人对 代理模式 没有搞清楚,本质上只要你弄懂了代理模式,这个by的所有问题也就迎刃而解了。所以今天这篇文章就从 java版本的代理模式入手,帮你捋清楚kotlin中的by!
这段代码有问题吗?
public class UserController {
//假设这来自依赖注入
DataReportCollector dataReportCollector;
public void login(String account,String password){
long startTime=System.currentTimeMillis();
//省略 真正登录的代码
long endTime=System.currentTimeMillis();
dataReportCollector.recordRequest(endTime-startTime);
}
public void register(String account,String password){
long startTime=System.currentTimeMillis();
//省略 真正注册的代码
long endTime=System.currentTimeMillis();
dataReportCollector.recordRequest(endTime-startTime);
}
}
简单来说,我们有个userController 里面有基本的登录注册功能,然后我们还对登录注册 增加了性能统计的埋点。统计了这2个接口的时间。 从业务功能上来说 没有问题。但在写法上有2个问题:
1.性能统计的代码 和我们的登录注册代码耦合在一起,以后 性能统计的代码要变更 会非常麻烦,我们还需要跑到userController里面来对照修改, 2.性能统计和 用户的登录注册代码 本质上不是一个东西,违反了 职责单一 原则。
第一种改进方式
首先定义一个接口
public interface IUserController {
void login(String account, String password);
void register(String account, String password);
}
然后实现我们的 用户 功能
class UserController2 implements IUserController {
@Override
public void login(String account, String password) {
//省略 登录逻辑
}
@Override
public void register(String account, String password) {
//省略注册逻辑
}
}
最后 实现我们的代理类
class UserController2Proxy implements IUserController {
private UserController2 userController2;
public UserController2Proxy(UserController2 userController2) {
this.userController2 = userController2;
}
//依赖注入
DataReportCollector dataReportCollector;
@Override
public void login(String account, String password) {
long startTime = System.currentTimeMillis();
userController2.login(account, password);
long endTime = System.currentTimeMillis();
dataReportCollector.recordRequest(endTime - startTime);
}
@Override
public void register(String account, String password) {
long startTime = System.currentTimeMillis();
userController2.register(account, password);
long endTime = System.currentTimeMillis();
dataReportCollector.recordRequest(endTime - startTime);
}
}
实际使用的时候 我们就使用这个proxy 类就可以了。 你看这样是不是 就简单的 完成了我们解耦的目的了?
有没有更简单的改进方法?
上面那个方法其实 有个缺点, 就是如果 userController 的代码 不受你控制,你无法改变他的代码,他又没有 一个IUser的接口 那怎么办呢? 其实也是有办法的。
class UserControllerProxy extends UserController {
//假设这来自依赖注入
DataReportCollector dataReportCollector;
@Override
public void login(String account, String password) {
long t1 = System.currentTimeMillis();
super.login(account, password);
long t2 = System.currentTimeMillis();
dataReportCollector.recordRequest(t2 - t1);
}
@Override
public void register(String account, String password) {
long t1 = System.currentTimeMillis();
super.register(account, password);
long t2 = System.currentTimeMillis();
dataReportCollector.recordRequest(t2 - t1);
}
}
只要extends 原来的UserController 就可以了。 这种写法 就可以解决 源代码不受你控制的情况了, 当然在有选择的情况下,还是建议 优先选择 使用接口的方式 来完成 代理模式,因为 时刻谨记 组合大于继承
Kotlin 完成代理模式
我们有了java 版本的 代理模式经验以后 来完成kotlin的 就容易多了。
class KUserProxy(private val userController: UserController2) : IUserController {
//依赖注入
lateinit var dataReportCollector: DataReportCollector
override fun login(account: String?, password: String?) {
//这里的代码就省略了 和 register 是差不多的
}
override fun register(account: String?, password: String?) {
val t1 = System.currentTimeMillis()
userController.register(account, password)
dataReportCollector.recordRequest(System.currentTimeMillis() - t1)
}
}
这个就是kotlin 版本的实现,也很简单, 有的人会问了, 哎呀 你烦死了 这个代理模式我已经会了 但是这和今天的by 关键字 有啥关系呢? 继续看,这次我们使用了by 关键字
class KUserProxy(private val userController: UserController2) : IUserController by userController {
//依赖注入
lateinit var dataReportCollector: DataReportCollector
override fun register(account: String?, password: String?) {
val t1 = System.currentTimeMillis()
userController.register(account, password)
dataReportCollector.recordRequest(System.currentTimeMillis() - t1)
}
}
这里解释一下 上面的程序, 我们使用了by 关键字以后 那么所有IUserController里面的 接口方法 在默认的情况下 都是使用了 userController的实现, 也就是使用了 我们传进去的 UserController2这个对象的实现。
那么有人又要问了,什么叫默认情况?
默认情况就是默认情况啊,比如上面的例子中,我们重写了register函数,这个就是非默认情况了,因为你重写了,所以register这个方法使用的就是你 重写里面的代码,是包含数据统计的。 但是你会发现 这里我们没有重写login方法, 那login方法默认实际运行的时候使用的就是UserController2的login代码了。
到这里有人又觉得奇怪了,好吧,看样子似乎有一点明白了,但是我仍旧没有感觉到你这个kotlin的 by 关键字方便在哪里啊?我们代理模式最终肯定还是要修改方法的,你要修改方法 不还是要重写方法么?不管你有没有使用by关键字 都得要重写方法啊,除非你想代理某个方法。
问题的关键就在这里了: 在实际开发中 我们的接口 往往可能不止一个方法,可能会有多个方法, 但我们在实际使用代理模式的时候,我们往往 不需要代理全部的方法,可能10个方法里面 只有2个方法 我们想代理用一下,其他都保持默认。 这种情况下,kotlin的 by关键字就很有用了,你使用了一个by关键字,其他默认的8个方法你都不需要管了,只需要重写你想代理的2个方法就可以。 但是如果你用java的代码来写,你需要将这10个方法都实现一遍,这种时候就很麻烦了。
想明白这个,你再看下 刚才那段kotlin代码的反编译代码 你对by关键字的理解 就超过8成了,剩下两成 无非是熟悉一下 by关键字的具体用法。
kotlin中的属性委托
有了上面的基础,我们再来学习属性委托 就简单许多了。 这里举几个简单的例子吧
class Delegate : ReadWriteProperty<Any, String> {
override fun getValue(thisRef: Any, property: KProperty<*>): String {
//不管实际的值是什么 我只返回这个固定的字符串
return "delegate string"
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
//打印一下 每次set的值
println("delegate get value$value")
}
}
class Dto {
var p1: String by Delegate()
}
实际使用一下;
var dto = Dto()
dto.p1 = "wuyue"
println(dto.p1)
看下执行结果,有了前面代理模式的铺垫 要理解这个就不难了。
这种写法实际上比我们 自己重写get和set方法 逻辑上要清晰的多。
如果是val的话,因为值是不可变的,所以对应的就是readOnly了
我们还可以加一些监听,很方便的实现观察者模式
var p2: String by Delegates.observable("要给对象一个默认值 否则第一次oldValue取不到"){
property, oldValue, newValue ->
println("property$property oldValue=$oldValue newValue=$newValue")
}
Delegates 还有一些其他的语法糖 比如 Delegates.null vetoable 等等 ,这些东西 随便搜搜 网上都有 我就不过多叙述了,反正你们理解了 代理模式, 理解这些东西就很容易。
by关键字 在android中 几个方便的使用小case
实现双击退出功能
private var backPressTime by Delegates.observable(0L) { pre, old, new ->
if (new - old < 2000) {
finish()
} else {
//show toast 再按返回键就退出
}
}
override fun onBackPressed() {
backPressTime = System.currentTimeMillis()
}
再举个例子 我们平常写代码的时候 经常要用到 SharedPreferences 对吧,每次对一个值 get set的时候 都要定义一个key,其实还是蛮麻烦的,有了by 这个关键字,我们轻松的 就可以解决这个问题了 让使用 sp就和 我们在dto中 取值 或者set值 是一样的
object SharedPreferencesUtils {
//设置一个单例的 preferences
var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(BaseApplication.getApplication())
var appName by SharedPreferenceDelegates.string("defaultValue")
}
private object SharedPreferenceDelegates {
fun string(defaultValue: String = "") = object : ReadWriteProperty<SharedPreferencesUtils, String> {
override fun getValue(thisRef: SharedPreferencesUtils, property: KProperty<*>): String {
return thisRef.preferences.getString(property.name, defaultValue) ?: ""
}
override fun setValue(thisRef: SharedPreferencesUtils, property: KProperty<*>, value: String) {
thisRef.preferences.edit().putString(property.name, value).apply()
}
}
}