扩展Result,让你的代码更简洁

屏幕截图 2024-06-12 235521.png

在使用 Kotlin 开发 Android中,我们一般返回 Result 来表示方法调用的结果。对于 Result 的返回值,我们可以很方便得使用 onSuccess 和 onFailure 等扩展方法来处理不同的结果。代码示例如下,可以看到使用 Result 可以让我们的代码变得非常简洁。

fun method1(): Result<Int> {
    if (isSuccess) {
        return Result.success(1) 
    } else {
        return Result.failure(Exception("method1 error"))
    }
}

fun main() {
    method1()
        .onSuccess { println("onSuccess $it") }  
        .onFailure { println("onFailure $it") }
}

虽然 kotlin 对 Result 提供了很多方法来支持结果的获取,但是对于如何处理两个及以上的 Result 之间关系,kotlin 就没有什么提供很好的方法。这么说,你可能听不懂,下面举一个例子。

比如说我们有一个更新用户的头像的需求。这里有两个步骤,第一步,上传头像获取新头像的 url,第二步更新用户的信息。

fun uploadFile(): Result<String> {
    // 上传头像
    ...
}

fun updateProfile(url: String): Result<Boolean> {
    // 更新用户信息
    ...
}

// 展示错误的界面
fun showErrorPage() {
    ...
}

// 更新用户的UI
fun updateProfileUI() {
    ...
}

fun main() {
    // 两个方法之间需要嵌套,代码不简洁
    uploadFile().onSuccess {  
        updateProfile(it).onSuccess {  
            updateProfileUI()
        }.onFailure {  
            showErrorPage()
        }  
    }.onFailure {  
        showErrorPage()
    }
}

可以看到,对于两个以上的 Result 之间的关系,Result 并没有提供相关的方法来处理。这就导致代码看起来非常难看。这篇文章,就介绍如何自己扩展 Result,处理多个 Result 之间的关系,让你的代码更简洁。

依赖关系

一般的开发中,最常见的关系就是依赖关系了。比如前面的例子,更新用户信息的接口依赖上传用户头像的接口。在开发过程,依赖关系大体分为三种,分别是 一对一、一对多以及多对一。

一对一依赖

一对一依赖简单得说就是,方法B依赖方法A的返回值。最开始的例子就可以说是一对一依赖关系。如何解决这种关系呢,我们可以为 Result 添加扩展方法 andThen,这样就可以把之前的 uploadFile 和 updateProfile 方法链接起来,代码如下:

uploadFile()  
    .andThen { updateProfile(it) }  
    .onSuccess { updateProfileUI() }  
    .onFailure { showErrorPage() }

当 uploadFile 或者 updateProfile 任意一个方法失败,最后会执行 onFailure 代码块里的 showErrorPage 方法。而当 uploadFile 和 updateProfile 都执行成功时,最后则会执行 onSuccess 代码块里的 updateProfileUI 方法。

这样看起来是不是简洁多了。如果你有更多方法需要串联,只需要往后面加一个 andThen 就可以了。扩展方法是不是非常方便,接下来我们来看看 andThen 是怎么样实现的。代码如下:

// 自定义异常,表示内部出错了
class InnerException(message: String = "inner value error"): Exception(message)

@OptIn(ExperimentalContracts::class)
public inline fun <V, E> Result<V>.andThen(transform: (V) -> Result<E>): Result<E> {
    // kotlin 约定,告诉编译器 transform 最多执行一次
    contract {
        callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
    }
    if (isSuccess) {
        val value = getOrNull() ?: return Result.failure(InnerException())
        return transform(value)
    } else {
        val exception = exceptionOrNull() ?: return Result.failure(InnerException())
        return Result.failure(exception)
    }
}

首先我们自定义了一个 InnerException 异常,这是因为在 kotlin 的 Result 中的 value 是 internal (模块内可见的)。我们只能通过 getOrNull 或者 exceptionOrNull 获取内部的值。通常情况下,isSuccess 为 true 时, getOrNull 一定有值,如果获取不到值就抛出 InnerException 异常,表示内部出错了。

inline 关键字则表示方法内联,是为了在循环中减少对象创建,避免性能问题。而 contract 以及注解 @OptIn(ExperimentalContracts::class) 属于 kotlin 约定。

一对多依赖

一对多依赖简单说就是,方法B 和 方法C 依赖方法A的返回值。比如说,你需要看你各个平台账号的视频播放量,这时就需要先获取你的手机号(一般来说平台账号就是手机号),再通过手机号获取不同平台的播放量,最后相加获得。代码示例如下:

// 获取用户手机号
fun getUserPhoneNumber(): Result<String> {
    ...
}
// 获取B站视频播放数据
fun getBilbilVideoPlayNum(phoneNumber: String): Result<Long> {
    ...
}

// 获取抖音的视频播放数据
fun getTiktokVideoPlayNum(phoneNumber: String): Result<Long> {
    ...
}

为了处理这种关系,我们可以为 Result 扩展 dispatch 方法,代码如下:

@OptIn(ExperimentalContracts::class)
public inline fun <V, E> Result<V>.dispatch(transform: (V) -> E): Result<E> {
    contract {
        callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
    }

    if (isSuccess) {
        val value = getOrNull() ?: return Result.failure(InnerException())
        return kotlin.runCatching {
            transform(value)
        }
    } else {
        val exception = exceptionOrNull() ?: return Result.failure(InnerException())
        return Result.failure(exception)
    }
}

这样,我们就可以使用 dispatch 扩展方法,来处理一对多的依赖关系,代码示例如下:

getUserPhoneNumber().dispatch { phoneNumber ->
    val bilbilVideoPlayNum = getBilbilVideoPlayNum(phoneNumber).getOrThrow()
    val tiktokVideoPlayNum = getTiktokVideoPlayNum(phoneNumber).getOrThrow()
    bilbilVideoPlayNum + tiktokVideoPlayNum
}.onFailure { println("onFailure $it") }
 .onSuccess { println("onSuccess $it") }

当 getUserPhoneNumber 、getBilbilVideoPlayNum 或者 getTiktokVideoPlayNum 任意一个方法出错时,就会执行 onFailure 中的代码;当三个方法都成功执行时,最后才会执行 onSuccess 中的代码。这样我们就可以简洁得对异常进行统一地处理。

多对一依赖

多对一依赖与一对多依赖正好相反,它是指方法C依赖于方法A和方法B的返回值。比如说,公司报销出差费用,需要发票单号;由于不同的人报销种类和费用不同,也需要获取个人的信息,代码示例如下:

// 获取个人信息
fun getUserInfo(): Result<UserInfo> {
   ...
}
// 获取发票单号
fun getInvoiceNumber(): Result<String> {
   
}
// 报销费用
fun reimbursement(userInfo: UserInfo, invoiceNumber: String): Result<Boolean> {
   ...
}

要处理这种关系,我们可以创建 zip 方法。代码如下:

@OptIn(ExperimentalContracts::class)
public inline fun <V> zip(block: () -> V): Result<V> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return runCatching {
        block()
    }
}

使用 zip 方法,我们就可以这样处理多对一依赖,代码如下所示:

zip {
    val userInfo = getUserInfo().getOrThrow()
    val invoiceNumber = getInvoiceNumber().getOrThrow()
    userInfo to invoiceNumber
}.andThen {
    reimbursement(it.first, it.second)
}.onFailure { println("onFailure $it") }
    .onSuccess { println("onSuccess $it") }

可以看到它和 dispatch 方法的用法非常相似,不同是 dispatch 是 Result 的扩展函数;而 zip 不是。

选择关系

可选关系简单地说就是二选一(或者多选一)。比如说手机付款,先用零钱余额支付,如果支付失败(比如余额不足)则使用花呗支付。代码示例如下:

// 使用零钱支付
fun payByChange(): Result<Boolean> {
   
}
// 是否花呗支付
fun payByHuabei(): Result<Boolean> {
   
}

处理这种选择关系,我们可以为 Result 创建 or 方法。代码如下:

public fun <V> Result<V>.or(result: Result<V>): Result<V> {
    return when {
        isSuccess -> this
        else -> result
    }
}

有了 or 方法,我们就可以简洁地处理选择的关系了。代码示例如下:

payByChange().or(payByHuabei())
    .onFailure { println("onFailure $it") }
    .onSuccess { println("onSuccess $it") }

Result集合

除了前文讲的依赖关系以及选择关系外,多个 Result 也可以组成一个集合。比如说你上传多张图片到朋友圈,每一张图片都可能上传成功或者失败,这时就获取到了不同图片的 Result 的集合。代码示例如下:

// 批量上传图片
fun uploadFiles(paths: List<String>): List<Result<String>> {
    ...
}

对于 Result 集合,我们可以做对其很多操作。比如说,如果我们只想知道有哪些图片上传成功了,就可以创建 valuesOf 的扩展方法,用来获取成功结果的列表,代码如下:

// 获取成功结果的列表
public fun <V, R : Result<V>> valuesOf(results: List<R>): List<V> {
    return results.asIterable().filterValues()
}

public fun <V> Iterable<Result<V>>.filterValues(): List<V> {
    return filterValuesTo(ArrayList())
}

public fun <V, C : MutableCollection<in V>> Iterable<Result<V>>.filterValuesTo(destination: C): C {
    for (element in this) {
        if (element.isSuccess) {
            val value = element.getOrNull() ?: continue
            destination.add(value)
        }
    }
    return destination
}

如果我们需要判断图片上传是否都执行成功了,我们可以创建 allSuccess 方法来判断是否全部执行成功。

public fun <V> Iterable<Result<V>>.allSuccess(): Boolean {
    return all(Result<V>::isSuccess)
}

集合的场景有很多,这里就不一一介绍了

支持协程

在一对多依赖的例子中,我们是同步获取不同平台的视频播放量。但在开发中,我们更期望是并发获取。代码示例如下:

getUserPhoneNumber().coroutineDispatch { phoneNumber ->
    val bilbilVideoPlayNum = async { 
        getBilbilVideoPlayNum(phoneNumber).bind() 
    }
    val tiktokVideoPlayNum = async { 
        getTiktokVideoPlayNum(phoneNumber).bind() 
    }
    bilbilVideoPlayNum.await() + tiktokVideoPlayNum.await()
}.onFailure { println("onFailure $it") }
 .onSuccess { println("onSuccess $it") }

我们来看看 coroutineDispatch 的实现。

// 由于 async 需要一个 CoroutineScope 来启动,这里创建一个Result的CoroutineScope
public interface CoroutineResultScope<V> : CoroutineScope {
    public suspend fun <V> Result<V>.bind(): V
}

internal class CoroutineResultScopeImpl<V> (
    delegate: CoroutineScope,
) : CoroutineResultScope<V>, CoroutineScope by delegate {

    private val mutex = Mutex()
    var result: Result<V>? = null

    // 使用 bind 支持协程的结构化编程,这样当一个协程任务异常失败时,
    // 取消其他的协程任务
    override suspend fun <V> Result<V>.bind(): V {
        return if (isSuccess) {
            getOrNull() ?: throw InnerException()
        } else {
            mutex.withLock {
                if (result == null) {
                    result =  Result.failure(this.exceptionOrNull() ?: InnerException())
                    coroutineContext.cancel()
                }
                throw CancellationException()
            }
        }
    }
}

@OptIn(ExperimentalContracts::class)
public suspend inline fun <V, E> Result<V>.coroutineDispatch(crossinline block: suspend CoroutineResultScope<E>.(V) -> E): Result<E> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    if (isSuccess) {
        lateinit var receiver: CoroutineBindingScopeImpl<E>
        return try {
            coroutineScope {
                receiver = CoroutineBindingScopeImpl(this)
                val value = getOrNull() ?: throw InnerException()
                with(receiver) {
                    Result.success(block(value))
                }
            }
        } catch (ex: CancellationException) {
            receiver.result!!
        }
    } else {
        return Result.failure(exceptionOrNull() ?: InnerException())
    }
}

总结

在这篇文章中,我们介绍了如何扩展 Result,来处理不同的结果分支,使我们的代码更加简洁。这种处理异常分支的方式叫做轨道编程(Railway Programming)。在 Github 中,也有很多用 Kotlin 实现轨道编程的项目,比如说 michaelbull/kotlin-result,它内部自己定义了 Result,而不是使用的 Kotlin 的 Result,本文的很多例子就是根据这个项目来改造的,有兴趣的可以看看。

参考

如何扩展 kotlin.Result 让代码更简洁

作者:小墙程序员
链接:https://juejin.cn/post/7379509948903014451
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 代码简洁之道中的测试题强调了测试代码的重要性以及测试代码应该保持整洁的原则。测试代码和生产代码一样重要,需要被思考、被设计和被照料。测试代码的整洁性对于保证生产代码的可扩展性、可维护性和可复用性至关重要。测试代码应该快速、独立、可重复、自足验证和及时编写。快速意味着测试应该能够快速运行,独立意味着测试之间应该相互独立,可重复意味着测试应该能够在任何环境中重复通过,自足验证意味着测试应该有布尔值输出,及时编写意味着单元测试应该在使其通过的生产代码之前编写。此外,基于经验和直觉推测程序中可能存在的各种错误,并根据这些错误选择测试用例也是一种常用的方法。通过列举可能的错误和容易发生错误的特殊情况,可以选择这些情况下的例子作为测试用例。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [《代码整洁之道 》第九章 单元测试](https://blog.csdn.net/weixin_39924752/article/details/127931527)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [软件测试面试题(含答案)](https://blog.csdn.net/qq_42434318/article/details/114263419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值