这几天在 review 同事的代码的时候,发现一块有意思的代码,我将其写成对应的伪代码如下:
代码语言:javascript复制class UserViewModel(val userUsecase: UserUsecase) {
// 根据 userId 获取 userName
fun getUser(userId:Int) {
val name = userUsecase(userId).name
}
}
class User(val name: String, val age: Int) {}
起初在看到这段代码的时候,觉得十分反人类,在 Kotlin 中,对象的初始化可以省略 new
操作符,也即类后面再配个 ()
即可,为啥一个初始化的对象还能继续用 ()
,在直观的感受下,我以为是初始化了一个对象,唯一让我觉得不像是初始化的就是 userUsecase 开头并不是大写,这才打消我认为他是初始化对象的疑虑。
在我想点进去看下根据 userId 获取 User 的过程,我无论追踪代码,都无法跳转到真正的逻辑代码调用处,点击 userUsecase 会直接跳转到 UserViewModel 的构造方法,点击 name 会跳转到 User 对象,这让我很苦恼。
我不得不点击 UserUsecase 类去看下里面的代码,这对于 review 人来说简直是灾难,但为了解决问题,先妥协,再一探究竟。
进入 UserUsecase 类,伪代码如下:
代码语言:javascript复制class UserUsecase {
operator fun invoke(userId: Int): User {
// 从数据库中根据 id 获取 User 数据
// 返回 User 数据
return User("lisi", 30)
}
}
看到了奇怪的 invoke 函数,并且使用了 operator 操作重载符,为了了解这种语法,我在 Kotlin 中文网查了下该语法的使用,在调用操作符章节中有所说明:
对象()
等价于 对象.invoke()
,()
内为函数的参数,也即我们上面的那段代码,可以翻译一下:
class UserViewModel(val userUsecase: UserUsecase) {
fun getUser() {
val name = userUsecase(1001).name
// 等价于
val name2 = userUsecase.invoke(1001).name
}
}
也可以用 Kotlin Decompile 看下结果:
image.png
需要说明的是,对象()
这种写法是有条件的:
- 必须用 operator 修饰方法
- 方法名称必须是 invoke
- invoke 参数可以多个,不做限制
由于 invoke 函数参数不加限制,这又带来了一个问题,如果重载了多个 invoke 函数,就更不知道业务方在调用的时候是做了什么事情,依然不得不进入代码才能知道逻辑。
上面的示例给的已足够简单,但实际在我们的业务中,比这还复杂,invoke 函数被封装到了父类,当我点进去的时候根本找不到 invoke 函数,只能往上查看父类有没有,在找到 invoke 函数时才发现,他最终调用了个抽象方法,该抽象方法由子类实现,我又不得不返回到子类查看这个方法,最终才敲定这个方法做了什么逻辑。
总结:
虽然 operator invoke 可以省略调用方写函数名这个过程,但需要注意的是,代码无论是类名还是方法名还是变量名,一定要做到见名识意,显然,他已经破坏了这个规则,让 review 人很抓狂。
我也很理解大家对 Jetpack 的热爱,这种写法在官方也有出现,可以参考 Domain Layer 这章。但我想说的是,省略方法名这个过程真的有必要吗?写代码到底是为了炫技还是为了让别人能看懂自己的代码呢?
[Domain Layer]https://developer.android.google.cn/jetpack/guide/domain-layer?continue=https://developer.android.google.cn/courses/pathways/android-architecture#use-cases-kotlin