技巧1:使用别名使业务类型表达更清晰
例如,我们有一个PhotoUploader接口,它拥有一个uploadPhoto方法用以上传图片数据,并返回一个String类型的URL:
interface PhotoUploader {
suspend fun uploadPhoto(bytes: ByteArray): Result
}
现在的问题是,如果这个接口的调用者在不看注释的情况下,如何知道这个方法的返回值代表的是一个URL呢?
这个时候,一个好用的小技巧就是使用 kotlin 别名来定义返回值的类型,例如:
typealias PhotoUrl = String
interface PhotoUploader {
suspend fun uploadPhoto(bytes: ByteArray): Result
}
这里我们使用 typealias 显示的定义一个String类型的别名为PhotoUrl,这样调用者在看到uploadPhoto方法的返回值时,就一眼能够看出它返回的是一个Url,而不是其他的什么东西。
再举个例子,比如下面代码:
// 返回值是一个Map, key代表用户id,value代表作者的名字
val result : Map
显然如果没有注释,没有人会知道你返回的是啥玩意。但是我们使用别名可以很好的解决这个问题:
typealias IdToAuthorName = Map
val result : IdToAuthorName = getFromSomeBusiness()
我们看到 IdToAuthorName 这个类型就一目了然了。
不得不说别名是一个非常好用的东西,它能够使复杂的类型表达简单化、清晰化,而且越复杂的类型越有优势。
技巧2:使用更加人道的变量命名
请看下面的代码:
data class User(val id: String, val name: String, val isOnline: Boolean)
fun messageOnlineUsers(usersInRoom: List
val partitionedUsers = usersInRoom.partition { it.isOnline }
partitionedUsers.first.forEach { user ->
user.sendMessage(message)
}
}
private fun User.sendMessage(message: String) {
//···
}
这个代码中 messageOnlineUsers 函数的意图是过滤出usersInRoom这个列表中的所有在线用户,然后将message群发给所有在线用户。这里使用了一个kotlin的集合类的扩展函数partition,它的作用是根据传入的条件把集合分成两部分封装成一个Pair, List
但这段代码可读性不是很好,当我们看到这段代码时,首先我们要去看一下partition函数的定义,然后还要理解一下整段代码的逻辑结构,最后在脑子里做一番思考挣扎之后才能明白过来:哦,原来partitionedUsers.first表示的就是过滤出来的在线用户啊~ 显然,这理解起来废了不少劲,对于头发数量非常金贵的程序员来说无疑是雪上加霜。
那有没有更好的方法呢?
我们可以使用 Kotlin 的解构语法来解决此问题。还记得什么是 Kotlin 的解构语法吗,回忆一下,它最多的出现场景是在使用数据类的时候,例如:
val book = Book(0, "Kotlin in Action", "Dmitry")
val (id, name, author) = book
所以对于上面的代码我们可以这样改一下:
fun messageOnlineUsers(usersInRoom: List
val (onlineUsers, offlineUsers) = usersInRoom.partition { it.isOnline }
onlineUsers.forEach { user ->
user.sendMessage(message)
}
}
这样,是不是无需多言,一眼就懂了呢,我们甚至都不用关心partition这个函数具体是干嘛的。
技巧3:使用属性代理简化读写逻辑
请看下面的代码:
class MainActivity : ComponentActivity() {
private val sharedPreferences by lazy {
getSharedPreferences("my_prefs", MODE_PRIVATE)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences.edit().putString("token", "Hello world").apply()
val token = sharedPreferences.getString("token", null)
println(token)
setContent {
MyComposeApplicationTheme {
}
}
}
}
这个代码中只是为了从SharedPreferences中读或者写一个字符串的值,但是有没有发现我们要实现这样一个小需求就要写一大坨的东西。
在没有使用 Kotlin 之前,使用Java的时候,或许你有封装过一些工具类来做类似的事情,但是下面我们看一下如何通过 Kotlin 的属性代理的方式来简化类似这种读写数据的逻辑。
做法很简单,我们只需要定义一个包装类,让其实现ReadWriteProperty接口,你需要实现两个接口setValue和getValue,然后将SharedPreferences的读写逻辑移动到对应的setValue和getValue中即可:
class SharedPreferencesDelegate(
private val context: Context,
private val name: String,
private val defaultValue: String = ""
): ReadWriteProperty
private val sharedPreferences by lazy {
context.getSharedPreferences("my_prefs", ComponentActivity.MODE_PRIVATE)
}
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return sharedPreferences.getString(name, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
sharedPreferences.edit().putString(name, value).apply()
}
}
然后我们可以为Context定义一个扩展函数来使用代理对象:
fun Context.sharedPreferences(name: String) = SharedPreferencesDelegate(this, name)
使用就很简单了:
class MainActivity : ComponentActivity() {
private var token by sharedPreferences("token") // 委托代理给上面定义的代理类
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
token = "hello world" // 会调用上面代理类中的setValue方法
println(token) // 会调用上面代理类中的getValue方法
setContent {
MyComposeApplicationTheme {
}
}
}
}
当然我们只是读取值的话,就可以这样写:
println(token)
这样是不是完全看不出它是从一个SharedPreferences中读取的,这样将读写的逻辑隐藏在了背后,开发者只需要读,但不需要关心具体是怎么读的,能够更好的把精力放在纯业务问题上。
当然这里只是使用SharedPreferences来举例,虽然Google目前已经不推荐使用它了(推荐使用新一代轻量级存储库DataStore,如需了解详情可参考Jetpack架构组件库:DataStore),但这不并妨碍本文以此作为示例来演示问题所在,如果你有其他的比较复杂的读写逻辑想要简化,其实处理方式是类似的。
如果你使用 Compose 开发,对于属性代理一定不会陌生:
class MainActivity : ComponentActivity() {
private var state by mutableStateOf(0)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
state++
}
}
Compose 中常用的状态定义所使用的 by mutableStateOf() 就是一种属性代理。
注意: 本文部分示例代码为了方便直观,都是直接在 Activity 中演示的,但是实际当中应当遵循良好的架构分层原则,避免出现 MVC 式的大杂烩,例如使用 Jetpack Compose 开发的话,上面的SharedPreferences应该放在 Data Layer 数据层,作为 Data Source 为 Repositories (存储仓库) 提供数据源,而其他UI层(如ViewModel)必须通过Data Layer层暴露的Repositories 来作为访问数据源的唯一途径(详情请参考Jetpack Compose 中的架构思想)。
参考链接
发表评论