Elm 中使用子模組的一種方法

YJ Park發表於2017-01-23

為什麼需要引入子模組?

隨著程式碼量的增加,相關邏輯日漸複雜,需要維護的狀態和傳遞的訊息也迅速的增加起來。

Elm 的架構文件中並沒有詳細說明如何組織比較複雜的專案,我調查中看到的文章中的方案也大多仍然需要模組間的耦合,實際使用中並不能得到滿意的效果。

期望達到的效果

首先需要做到的是程式碼層面的分離,模組內部實現細節的修改對外部來說盡量不可見,減少程式碼的耦合程度,便於開發。

下一個階段的目標是模組的可重用性,除了簡單的函式層面的重用,在更高層次上也有很多相似性,例如如果由於應用場景的考慮,需要釋出多個微信小程式的話,其中有不少邏輯是可以共用的,例如微信端使用者登入、資訊獲取,服務後臺的 Session 管理,等等。

模組之間的互動應儘量簡單,可以用可維護的方式進行組織。

如何拆分子模組

個人的習慣是先從資料開始設計,在 Model 的部分先做分割,之後進行 Msg 的設計,宗旨是把聚合度高的部分放在一起,封裝成獨立的模組。

子模組間如何互動

多個模組需要彼此協調才能完成完整的應用邏輯,根據具體情況有以下的情境

資料依賴

某個模組需要外部提供所需的資料,有幾種處理的方法,可以根據具體需要進行選擇

  • 作為輸入事件的引數傳遞進來,只在相關事件的處理中使用
  • 封裝成內部的資料,加入 Model,在需要時訪問
  • 作為 update 方法的引數,每次更新時都可以訪問到

事件觸發

對於子模組來說,其實不用瞭解事件的具體來源,可以是模組自身,可以是其它模組,或是應用層面的使用者輸入。只要把自身的生命週期管理好即可,由於 Elm 架構的函式式和不可變特性,一般來說除錯也很方便,只要觀察 Msg 的序列以及相應的 Model 的變化往往就能找到問題所在。

WxApp 子模組

由於所有的微信小程式都需要進行使用者身份的管理,在 elm-wx-app 中提供了一個基本的身份認證子模組,在 API 呼叫之上提供了更高一層的介面。

下面列出了部分的程式碼,結構相對比較簡單,感興趣的話可以 Clone 完整的版本。

(目前的版本還比較簡單,介面也沒有完全固定下來)

Model

type alias Type =
    { systemInfo : SystemInfo.Type
    , userCode : String
    , userInfo : UserInfo.Type
    , userSecret : UserSecret.Type
    , tabs : List UiTab.Type
    , currentTabKey : UiTab.Key
    , pages : List UiPage.Type
    }

Msg

type Msg
    = DoInit
    | DoGetSystemInfo
    | DoCheckSession
    | DoLogin
    | DoLoadWxModel
    | DoGetUserInfo
    | GetSystemInfoMsg (Result Error GetSystemInfo.Msg)
    | CheckSessionMsg (Result Error CheckSession.Msg)
    | LoginMsg (Result Error Login.Msg)
    | LoadWxModelMsg (Result Error WxModel.Type)
    ...

update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        DoInit ->
            ( model
            , cmd DoGetSystemInfo
            )
        DoGetSystemInfo ->
            ( model
            , GetSystemInfo.cmd GetSystemInfoMsg
            )
        DoCheckSession ->
            ( model
            , CheckSession.cmd CheckSessionMsg
            )
        DoLogin ->
            ( model
            , Login.cmd LoginMsg
            )
        ...

WxApp Wrapper 例項分析

wrapper

wrapper 的細節請看 elm-component-updater 的實現程式碼,基本上是從主模型中訪問子模型(get),呼叫子模組的 update,之後再把返回的子模型更新到主模型中(set)

wrapper : Wrapper Msg Wx.Msg
wrapper =
    wrap WxMod
        { get = Just << .wx
        , set = \modModel model -> { model | wx = modModel }
        , update = Wx.update
        , react = reaction
        }

cmd msg =
    toCmd msg
        |> Cmd.map wrapper

reaction

reaction 的目的是對於特定的子模組事件產生相應的外部事件,來達到對其他模組的控制。

reaction modMsg modModel model =
    model ! []
        |> case modMsg of
            Wx.PopPageMsg pageKey (Ok _) ->
                case List.length modModel.pages of
                    0 ->
                        addCmd <| cmd <| Wx.SwitchTab "dialogue"
                    _ ->
                        noOperation
            _ ->
                noOperation

主應用中的相關程式碼

model

首先是在 Model 中包含子模組的部分

type alias Type =
    { rev : Int
    , wx : Wx.Model
    ...

事件定義

import Updater
import WxApp.Mod as Wx
type alias Delegate = (Updater Model Msg)

type Msg
    = WxMod Delegate
    | WxMsg Wx.Msg
    ...

WxMod 代表由 Wrapper 處理的事件,WxMsg 則是普通事件,需要在 update 中轉換為 Wrapper 事件。

這裡做區分的原因是在 Elm 中無法迴圈 import,在被 WxApp Wrapper 引用的程式碼中如果也需要通知 WxApp 的模組,則只能產生一個 WxMsg 型別的事件。

update

import Wrapper.Wx as Wx

updateMod : Msg -> (Model, Cmd Msg) -> (Model, Cmd Msg)
updateMod msg (model, cmd) =
    case msg of
        WxMod delegate ->
            delegate model
        WxMsg msg ->
            (model, Wx.cmd msg)
        ...

可以看到這裡對於 WxMsg 型別的事件使用 wrapper 做了一次轉換,略顯繁瑣,不確定是否有更好的方式。

總結

實際開發中應用以上方式寫了不少程式碼,在模組的分隔上感覺還是一種不錯的方法。

在需要調整模組結構的情況下,Elm 作為靜態型別語言提供了很大的幫助,編譯器可以發現不匹配的介面,重構起來有一氣呵成的感覺。

附錄

連結

相關文章