Runner in the High

技術のことをかくこころみ

ElmでサードパーティーなUI系モジュールを使う際にはTEAを前提にしたインターフェースでラップする

Elm Packagesで公開されているモジュールの中にはTEAでいうところのviewやmodelしか提供されていないものがあったりする。

これがコレクション系モジュールだとか関数単位での機能提供が目的のモジュールであればまだ問題ないが、UIを提供するタイプのモジュール(日付選択のカレンダー、トースト、など)だと話が変わってくる*1

モジュールによっては「仕様上今はupdate関数を提供する必要がないだけ」の可能性もあり、バージョンアップによって独自のMsg型とそれをハンドリングするupdate関数が将来的に現れる場合がある。こうなると利用している箇所すべてにupdate関数のハンドリング処理を追加する必要がでるなど、サードパーティーのバージョンアップによる修正作業が生まれる。

このようなケースを回避するために、UI系のサードパーティー・モジュールは必ず自前のモジュールにラップすることが望ましい。まずこれだけでサードパーティーによる影響範囲を1モジュールにカプセル化できる。

さらに、ラップするモジュールはあらかじめTEAの構成要素となる関数(model, update, view)をモジュールのインターフェースとして提供しておく。こうすることで、仮にupdate関数などを提供していないサードパーティー・モジュールから、提供しているモジュールに乗り換えたりしても、ラッパー・モジュールが変更を吸収することができるため、アプリケーションの大部分は影響を受けない。

仮に現時点で部分的に移譲する実装がサードパーティー・モジュールから提供されてない状態であっても、コードコメントにプレイスホルダーであると書いておけばいい。

module MyWidget exposing (Model, Msg, update, view)

import ThirdPartyWidget

{- 
    ThirdPartyWidgetのラッパー・モジュール

    今時点の実装ではModelとviewのみ内部実装をThirdPartyへ移譲している
    今後ThirdPartyを差し替えたりバージョンアップの際にTEAなモジュールになった場合には
    update関数とMsg型の実装をサードパーティー・モジュールの実装へ移譲する
-}


type Model =
    Model ThirdPartyWidget.Model


type Msg =
    NoOp


update : Model -> Msg -> ( Model, Cmd msg)
update model _ =
  ( model, Cmd.none )


view : Model -> Html msg
view (Model value) =
    ThirdPartyWidget.view value

Elmの素晴らしい点は、どんなに複雑なUIであっても必ずTEAのパターンに帰結するという点。

つまり、TEAから外れるモジュールは原則存在しえない。だからこそ、最も柔軟なインターフェースとしてTEAを構成するモジュールの形にしておけば、最終的にどんな変更が入ったとしても理論上はラッパー・モジュール内部で変更を吸収できる。

サードパーティー・モジュールをラップするという考え方

なお、サードパーティーなモジュールをラップするという考え方は特段Elmに限った話ではない。

dry-rbシリーズ*2の作者も「常に自分でコントロール可能なインターフェースにだけ依存させろ」と言っているし、安定依存の原則を考えたらアプリケーション開発では当然の話だと言える。どこか一カ所でしか使わないならまだしも、複数個所で使うようなサードパーティーの機能は必ず自前のモジュールとしてラップしておくことが望ましい。

izumisy.work

*1:カレンダーだとかドロップダウンセレクタなどのUI系モジュールはユーザーからのクリックや入力のようなElmランタイムを経由するライフサイクルが絡むことが多いため、機能が増えるとupdate関数が出現するケースがある

*2:Rubyアプリケーションにモナドやバリデーション基盤などを導入するためのユーティリティ系gem https://dry-rb.org/