这两天看了一份关于Monad
的PPT,将使用Monad
比喻成了面向轨道编程,觉得写的挺好的,周末特意写篇文章记录一下。首先我们看一段代码,这段代码模拟了一个处理request
的业务逻辑:
// 模拟处理业务
fun executeRequest(request: Request) : String {
// 校验身份
validateRequest(request)
// 处理业务
dealBiz(request)
// 存储数据
saveToDB(request)
// 发送消息
sendMessage(request)
return "success"
}
看起来这些代码很普通,但是在平时的业务代码里运行起来通常会遇到各种异常,也就难免我们需要很多的数据判断来避免这些异常破坏我们的逻辑了。所以实际的代码很可能会写成这样:
// 模拟处理业务
fun executeRequest(request: Request) : String {
// 校验身份
val isValidate = validateRequest(request)
if( isValidate ) {
return "Request is not valid"
}
// 处理业务
dealBiz(request)
try {
// 存储数据
saveToDB(request)
} catch (exception:Exception) {
return "Occur error when save results to DB"
}
// 发送消息
val isSendSuccess = sendMessage(request)
if (isSendSuccess == false) {
return "message send unsuccessfully."
}
return "success"
}
executeRequest()
函数在根据业务组装自己的控制流,实际的业务代码中有很多这种判断,为了避免执行到不应该被执行到的代码。但这种判断越来越多,代码就越难维护了。有一个笑话称这种代码是“上帝代码”,除了自己和上帝没有人能看懂,过了一段时间之后,只有上帝能看懂了。
那么如何让这些代码变得简单易读呢?先看一个例子,假设有两个函数,他们的作用如下:
- 牛 --> 牛肉
- 牛肉 --> 牛肉干
可以把函数的处理想象成铁轨,就像下面这样:
然后我们再把这个两个函数合并一下:
函数总是的执行总是两种情况,成功或者失败。这个函数执行的过程可能不会这么顺利,也许制作牛肉的过程就会有发生异常,也许制作牛肉干的过程会失败。所以可以定义一个Result
做Monads
容器,接收返回值,Result
定义的时候泛型可以指定两个类型一个是正常返回类型(Result.Success
),另外一个携带是Exception
的返回类型(Result.Failure
)。如果函数正常执行,就是用success()
函数处理,如果中间有一个失败了,则是用failure()
函数处理。
val result : Result<Boolean,Exception> = Result.of( 1 + 1 = 2 )
result.success {
// 处理正常业务逻辑
}
result.failure {
// 处理错误
}
这个时候,函数的执行就像在两条轨道上一样,一旦函数出现错误,执行函数的“火车”就可以驶向专门处理错误的轨道上一样:
那么我们再回到之前的例子,利用Result我们可以先把业务函数(即validateRequest()
,dealBiz()
等等)全部设计成返回Result
,组装这些业务函数的方法就可以这样写了:
fun executeRequest(request: Request) : String {
val result = Result
.of(request)
.flatMap { validateRequest(request) }
.flatMap { dealBiz(request) }
.flatMap { saveToDB(request) }
.flatMap { sendMessage(request) }
result.fold(
success = { return "success" },
failure = { return it.message }
)
}
我们把异常定义在业务函数中,直接利用Exception的message抛出,交由上层统一抛出。代码看着清晰了很多。特别是每个flapMap代码块中都在处理各自的业务,然后也可以将结果传递给下一个代码块。这里解释一下Result
中的map
函数和flatMap
函数:
- map() : 将函数的结果计算完毕之后,转换成一个新的
Result
对象返回。 - flatMap() : 将函数的结果计算完毕之后直接返回,比如结果是true,那么直接返回一个布尔类型(Boolean)。
以上两个函数如果遇到带有异常的Result
,会直接将这个异常的``Result返回。而下面两个函数是专门用来处理失败的Result
的
- mapError() : 将函数新的异常捕获之后,转换成一个新的
Result
带有新异常的对象返回。 - flatMapError() : 将函数的结果获得的新异常返回。
同样,如果遇到成功的Result
,那么函数会直接将这个成功的Result
返回。之前也看过很多关于Monads的文章,虽然看着很厉害的样子,但是一直不知道为什么要去使用Monads,其实就是为了让代码的业务逻辑和控制能分的更加清楚一些。之前读过一篇文章说,编程范式的本质是有效地分离Logic
,Control
和 Data
,即:
-
Logic
: 就是一般的业务代码,类似上面代码中的dealBiz()
,sendMessage()
等等 -
Control
: 对业务逻辑的流程控制,比如遍历数据、查找数据、多线程、并发、异步等等 -
Data
:函数和程序之间传递的这部分信息
所以面向“轨道”编程,就是设计了Result
这样一套模型来分离了Logic
和Control
。
Ps. 文中使用的是一个根据面向轨道设计Kotlin
库:https://github.com/kittinunf/Result