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