用Kotlin Result编程: 返回Result - 1

483 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 24 天,点击查看活动详情

用Kotlin Result编程: 返回Result

如何通过返回值来解决异常的所有问题. 对Result类的介绍, 以及它如何使处理意外错误变得轻而易举. 关于它与实际函数式编程的关系的简要说明.

以下是我们想要解决的具体问题:

  1. 我们想保留正常的控制流. 这意味着我们不希望执行过程从发生错误的地方跳到一千光年之外.
  2. 我们要处理所有的错误状态, 而不仅仅是"典型"的错误.
  3. 我们希望尽可能地将我们的代码与外部代码抛出的特定异常解耦.
  4. 我们想写更少的代码.

虽然这看起来是个很高的要求, 但实际上并非如此.

这里有一些观察:

  1. 当我们通过返回一个值来退出函数时, 正常的控制流得以保留. 当我们停止抛出异常时, 我们就没有其他方法来退出函数了, 所以当一个异常被抛出时, 我们需要尽快停止重新抛出它. 由于异常几乎无一例外(哈), 最终都会被转换为数值, 我们真正要说的是, 我们需要这种转换尽可能地发生在错误发生的地方 - 也就是说, 我们需要将进行数值转换的代码尽可能地深入.
  2. 1.在99%的情况下,你所做的只是将Throwable转化为一个字符串表示,并被发送到客户端(也许还被写入日志). 出于这个原因, 我们可以直接捕获Throwable(这实际上是可以做到的, 有某些限制), 而不必担心其具体类型.
  3. 1.剩下的1%的情况是特定业务需求的结果, 这使得它们成为业务(即服务)逻辑的一部分. 如果一个业务需求是"当具体的错误X发生时,做Y", 那么编写与X被抛出紧密耦合的服务代码是绝对可以的, 只要它是在所有其他业务逻辑旁边实现.

所以, 让我们从重新抛出异常切换到返回值. 要做到这一点, 我们需要一个数据类型来表示两种情况.

  1. 代码顺利完成, 结果是一些value: T, 以及
  2. 发生了一个oops.

首先想到的是Optional, 它在语义上等同于T?, 然而这对我们来说还不够丰富. Nullables/optionals可以用来表示成功, 但如果出了问题, 我们可能想编码一些关于出错的信息 - 可以说是"oops实例".而nullables/optionals只有传达"有"与"无"的能力. 我们想要的是Success(value: T)Failure(oops: Throwable).

所以...让我们来做这个! 让我们创建一个类型Result和两个子类Success(val value: T) : ResultFailure(val error: Throwable) : Result, 并使用这些来包装我们所有的计算.

sealed interface Result<out T>
data class Success<T>(val value: T): Result<T>
data class Failure(val error: Throwable) : Result<Nothing>

// Before
fun retrieve(id: Long): Product = try {
   productRepository.retrieveById(id)
} catch (e: ObjectNotFoundException) {
    throw ProductIdDoesntExist(e)
}

// after
fun retrieve(id: Long): Result<Product> = try {
   Success(productRepository.retrieveById(id))
} catch (e: Throwable) {
   Failure(e)
}

嗯...我们可以做得更好一些吗? 是的, 可以:

fun <T> runCatching(block: () -> T): Result<T> = try { 
    Success(block()) 
} catch (e: Throwable) { 
    Failure(e) 
}

fun retrieve(id: Long): Result<Product> = runCatching {
   productRepository.retrieveById(id)
}

未完, 待续...

用Kotlin Result编程: 返回Result - 2

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 24 天,点击查看活动详情