經過使用函數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的方式提供了這種支持,但目前多數都是用在了集合處理上。開發

相關文章
相關標籤/搜索