通常,在进行业务开发的时候,我们要对传入接口的参数进行层层验证,比如一个注册新用户的过程:
- 校验用户填写的表单项是否允许为空,格式是否满足要求
- 检查用户输入的验证码是否与会话中的验证码一致。
- 检查用户名是否已经存在
- 对用户信息进行填充和调整(比如密码要将明文加密)
- 将调整后的用户信息写入到持久化层
- 检查持久化层的返回结果,返回最终注册结果
在Java中,上述过程很容易描述成if ... return ... 的结构:
public Result register(UserInput userInput) {
// 验证表单填写
Result validateRs = validator.validate(userInput);
if (!valdateRs.isSuccess()) {
return validateRs;
}
// 检查验证码
Result checkCaptchaRs = captchaChecker.check(
userInput.getSessionId(), userInput.getCaptcha());
if (!checkCaptchaRs.isSuccess()) {
return checkCaptchaRs;
}
// 检查用户名是否已经存在
boolean existed = userDAO.isUsernameExisted(userInput.getUsername());
if (existed) {
return Result.badRequest("username_existed", "用户名已经存在");
}
// 对注册信息进行填充调整
User user = fillNewUserInfo(userInput);
// 持久化
try {
userDAO.save(user);
} catch (PersistentException e) {
return Result.sysError(e.getMessage());
}
// 返回注册成功以及新用户信息
return Result.success(ImmutableMap.of("user", user));
}
可以看到,在以上代码中,一旦有条件不满足,我们便会立即向调用者返回错误结果,if判断的都是不满足要求的情况以避免过度嵌套来增强可读性。
但在erlang里面,这样做就有些难度了,erlang虽然提供了case of这种判断,但是没有return语句,无法在条件不能满足时去显式的return,只能case of 层层嵌套,当判断逻辑复杂时代码简直惨不忍睹,假设上述注册过程在erlang中用一个函数实现:
-spec(register(UserInput :: #user_input{}) -> {ok, #user{}} | {error, Reason :: term()}).
register(UserInput) ->
%% 检查用户输入
case validator:validate(UserInput) of
{error, _Reason} = ValidateError -> ValidateError;
ok ->
%% 检查验证码
case chaptcha:check(UserInput) of
{error, _Reason} = ChaptchaError -> ChaptchaError;
ok ->
case user_storage:is_existed(UserInput#user.username) of
...
很快,我们就把一个简单的注册逻辑写成了传说中的那种“箭头型”代码,读起来太累了,这还才是这么点儿逻辑,那要是遇到更复杂的场景,该咋办?
还好几天前稍微瞄了一眼Elixir的一本教程,里面提到了这样一个观点 —— “编程时要重点关注数据转换,借助管道来组合转换,函数是数据转换器。”
对!就是管道!想想Linux里面,每个程序都有正确和错误的返回值,但使用管道的时候我们从来不需要对命令返回结果的正确错误做判断,这些事情都是由管道自身去做的,只要组成管道的程序输入输出符合约定,那么最终经由管道组合起来的程序再复杂,它也会是一个“顺序结构”。于是,照着这个想法,就构建出了这样的管道函数:
sync_pipeline([FirstFunc|TailFuncList]) ->
sync_pipeline(TailFuncList, FirstFunc()).
sync_pipeline([HFunc|TailFuncList], LastResult) ->
case HFunc(LastResult) of
%% 处理正确结果
NewResult when NewResult =:= ok; NewResult =:= true -> sync_pipeline(TailFuncList, NewResult);
{Rs, _Data} = NewResult when Rs =:= ok; Rs =:= true -> sync_pipeline(TailFuncList, NewResult);
%% 处理错误结果
NewResult when NewResult =:= error; NewResult =:= err; NewResult =:= false -> NewResult;
{Rs, _Data} = NewResult when Rs =:= error; Rs =:= err; Rs =:= false -> NewResult
end;
sync_pipeline([], LastResult) ->
LastResult.
很简单,上述代码所描述的即管道是由函数列表组成的,第一个函数的调用不需要参数,其它的函数以上一个函数的调用结果作为参数,函数返回的结果必须满足约定,ok、{ok, term()}、true、{true, term()}都是正确结果的模式,而err、error、false、{err, term()}、{error, term()}、{false, term()}都是错误结果的模式,当一个函数返回错误模式的结果时,函数链的调用就会终止,但如果顺利,就会以最后一个函数的结果作为管道的最终结果。通过使用sync_pipeline来实现的注册流程:
register(UserInput) ->
sync_pipeline([
fun() -> validator:validate(UserInput) end,
fun(_Result) -> chaptcha:check(UserInput) end,
fun(_Result) -> user_storage:is_username_exsted(UserInput#user.username) end,
...
])
通过这样的改造,一连串的判断最终变成了清晰的顺序结构~ 各种一目了然~ 其实这种做法不仅适应于erlang,任何判断条件复杂并且拥有匿名函数的语言都可以尝试通过这种pipeline的方式去重构代码,当然很多语言也已经内置集成了这种方式,比如haskell和elixir,Java也通过Streaming API的方式提供了这种支持,但目前多数都是用在了集合处理上。