经过使用函数pipeline来改善erlang代码中的case of嵌套

一般,在进行业务开发的时候,咱们要对传入接口的参数进行层层验证,好比一个注册新用户的过程:java

  1. 校验用户填写的表单项是否容许为空,格式是否知足要求
  2. 检查用户输入的验证码是否与会话中的验证码一致。
  3. 检查用户名是否已经存在
  4. 对用户信息进行填充和调整(好比密码要将明文加密)
  5. 将调整后的用户信息写入到持久化层
  6. 检查持久化层的返回结果,返回最终注册结果

在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
                         ...

很快,咱们就把一个简单的注册逻辑写成了传说中的那种“箭头型”代码,读起来太累了,这还才是这么点儿逻辑,那要是遇到更复杂的场景,该咋办?code

还好几天前稍微瞄了一眼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来实现的注册流程:ip

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的方式提供了这种支持,但目前多数都是用在了集合处理上。开发

相关文章
相关标签/搜索