开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情
续接上篇: 用Kotlin Result编程: 返回Result - 1
通过引入两个简单的类型和一个函数, 我们能够做到:
- 恢复正常的执行流程
- 摆脱诡异的"远距离动作"异常处理程序
- 编写的代码更短, 更干净, 而且几乎和我们完全忽略异常的代码一样简单
- 处理所有可能的错误, 现在和将来都是如此
- 明确这个操作可能会失败, 并使用类型来强制调用代码来处理这些失败(顺便说一下, 这就是检查异常的尝试, 而且大部分失败, to do)
然而, 最重要的是, 我们已经改变了我们的思维方式. 我们不再把异常看作是控制流结构, 我们把它们看作是简单的信息载体 - 基本上是数据类. 我们不会立即觉得有必要对它们进行翻译或链接, 除非有实际需要提供更多的信息或对它们作出反应--在大多数情况下, 类型并不重要.
当你开始处理以不同方式表示错误的库时, 这种方法的好处会成倍增加.
这里有一个例子:
// Returns an Int, throwing SomeException if it cannot do so
fun libFun1(): Int {
// ...
}
// Returns an Int if it is available, or an empty optional if it is not available. Throws SomeOtherException if
// argument is invalid
fun libFun2(argument: Int): Optional<Int> {
// ...
}
鉴于这些函数,我们的任务是创建ratioOfLibFuns(), 返回两个库函数的Int比率, 并适当处理错误. 在这个特定的场景中, 如果libFun2返回的Int丢失了, 这意味着用户忘记了做什么, 我们要以某种方式传达这个信息.
下面是我们到现在为止可能会实现的方式:
fun ratioOfLibFuns_oldWay(argument: Int): Int {
try {
val int1 = libFun1()
val int2 = libFun2(argument).orElseThrow {
UserRatioException("Int2 was not entered by user")
}
if (int2 != 0) {
return int1/int2
} else {
throw GenericRatioException("Int2 is 0!")
}
} catch(e: SomeException) {
throw GenericRatioException("Int2 is not available", e)
} catch(e: SomeOtherException) {
throw GenericRatioException("Argument to libFun2 is invalid!", e)
}
}
唉, 真是一团糟. 值得庆幸的是, 利用我们刚刚发现的东西, 我们可以做得更好:
fun ratioOfLibFuns_newWay(argument: Int): Result<Int> = runCatching {
val int1 = libFun1()
val int2 = libFun2(argument).orElseThrow {
UserRatioException("Int2 was not entered by user")
}
int1 / int2
}
通过引入一个超级简单的类型, 我们成功地使代码缩短了4倍, 提高了它的可读性和可维护性, 并明确了它可能失败的事实.
事实上, 我们可以走得更远 - 我们可以将这种方法与我们学到的强类型非法状态相结合,并完全避免使用UserRatioException.
sealed interface Ratio
data class RatioOf(val result: Int): Ratio
object Int2Missing: Ratio
fun ratioOfLibFuns_newWay(argument: Int): Result<Ratio> = runCatching {
val int1 = libFun1()
libFun2(argument)
.map<Ratio> { int2 -> RatioOf(int1 / int2) }
.orElse(Int2Missing)
}
请注意, 仅仅通过阅读这个函数的签名, 你就获得了很多信息: 它返回一个Ratio实例, 可以识别单一类型的业务错误(Int2Missing), 并被包裹在Result中, 这意味着这个方法可能会意外失败. 甚至不用看实现, 你就能立即知道这个方法中可能发生的所有事情.
这是使用具有隐含意义/行为的类型所带来的令人难以置信的强大后果之一. 你其实很清楚这一点, 但可能没有意识到. 想想看 - Optional意味着一个值的存在或不存在, List意味着多个值, Future是一个将在某个时间点存在的值, Function是一个可以被产生的值, Result是一个值或一个失败, 等等.
这实际上是实际函数式编程的核心原则之一 - 使用这些数据类型, 以及许多其他的数据类型, 如Either, Writer, State, Eval和许多其他的数据类型, 来表示程序中的行为, 并将我们需要的东西作为这些基本的, 可证明的正确行为的组合. 这个话题远远超出了本文的范围, 但我认为值得一提的是, 它与我们在这里做的Result有密切联系.
事实证明, Result层次结构和runCatching方法已经是标准库的一部分. 在下一篇文章中, 我们将更仔细地探讨标准库中包含的内容.
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情