Runner in the High

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

Elmアプリケーションにおけるモジュールレベルでの詳細設計

Elmアプリケーションで比較的モジュール多めなアプリケーションの機能開発をするときに同僚とトライしている手法について。

言語的なElmのテクニックみたいな話ではなく、どちらかといえばもっと抽象的なハナシ。

1. 画面からざっくりとモジュールを見つけ出す

基本的に新しく機能を設計するときには画面設計みたいなものがデザインレベルで上がってきているはずなので、それを元に画面を構成するモジュールを分解する。この時点では画面ベースでやる。

自分がエンジニアHubで寄稿したElm記事でも、まずは画面をベースにして主要なモジュールを見つけ出している。

https://cdn-ak.f.st-hatena.com/images/fotolife/b/blog-media/20200302/20200302130323.png

2. モジュールを分類する

見つけ出したElmのモジュールを"TEAなモジュール""そうでないモジュール"で分類する。依存関係も見つける。

"TEAなモジュール"とはMsg型とupdate関数を持つTEA的なライフサイクルに乗るモジュール。"そうでないモジュール"はその名の通りそれ以外のもの。

TEAなモジュールを見つけ出すことはかなり大事で、ElmアプリケーションにおけるTEAなモジュールは必ず大元になるApp.elmだとかMain.elmみたいな上位のモジュールのupdate関数から自分のupdate関数に処理を移譲してもらう必要がある。なので、依存関係をあぶり出す段階でTEAモジュールの依存元になるモジュールにTEAなものが無ければ設計は破綻しており、この時点でモジュールの依存関係がミスっているということが分かる。

意外と見落としがちなのがPorts周り。update関数だけではなくsubscriptions関数も依存元モジュールにTEAなモジュールが必要であることに注意。また、Ports経由でデータの取得依頼とその結果の受け取り待ちをするような機能がある場合には、それらの処理のPortsどのようなポリシーの元でどのモジュール内に定義するかを考えておく。Ports定義はTEAなモジュールの中だけに限定するなど。

ElmにおいてPortsはかなり飛び道具的な機能であるため、ここのルールがガバると複数人開発ではすぐカオスになる危険性がある。

サードパーティー・モジュールの取り扱い

サードパーティーのモジュールを導入する際には移譲するためのラッパー・モジュールを用意する。

このモジュールはサードパーティー側のインタフェース設計によらずTEAなモジュールであることが望ましい。理由は、依存先のモジュールが変更されても最大公約数的に影響範囲を抑えられるようなインターフェイスを提供するため。詳しくは以下の記事で解説している。

izumisy.work

3. モジュールのインターフェイスを設計する

見つけ出したモジュールのインターフェイスをざっくりと書き出していく。

細かい実装までは書かない。

module Page exposing (Model, Msg, init, update, view)

-- model

type Model
    = Model

init : Model

-- update

type Msg
    = Msg

update : Msg -> Model -> ( Model, Cmd msg )

-- view

view : Model -> Html msg

上記はTEAなモジュールのインタフェース設計の例だが、ここでは外部のモジュールから依存される関数や型を意識する。外部のモジュールはどういう風にデータを取り出したいだろうか?というのを考えて関数を作っていく。これを考え始めると自ずとModelの設計が必要になるため必要に応じてModelも詳細化していく。なお、この時点ではモジュール内部でしか使わない非公開関数は書いても書かなくてもいい。

このインタフェース設計の時点でカプセル化したモジュールのインターフェイスを追求していこうとすると、ちらほら「結局exposingですべてのデータも関数も公開しなくちゃダメだ」みたいなモジュールが現れる。exposingで全公開 exposing(..) するようなモジュールは前提としてNG。これは、その実装をモジュールへ分割する意味がないモジュールの責務が薄いことを示している。そのようなモジュールは解体して他モジュールへ統合するか、依存元から責務を移動してみる*1。モジュールを作る理由がなければ作らなくてもいい。

モジュール設計に「機能による区分」を積極的に採用してしまっている場合にも、責務の薄さが表れることがある。モジュールの区分軸の話は過去記事を参照のこと。

izumisy.work

ここまで事前に設計段階で詰めておけば、残りは画面と具体のロジックを実装することに集中できる。また、作業の分担などを行ったり引き継ぎを行う際も、設計まで固められているので知識レベルを揃える工数を少なくできる。

あとはこれを関係者にレビューしてもらうなどして設計の妥当性を高めていく。

思うこと

Elmは他のフレームワークと比べて「できないこと」が多い分、設計の際になにをどこに置くべきか、みたいな観点が明確になる。設計段階でモジュール設計に時間を使って矛盾点をあぶり出すことで「この機能って本当にこの設計で作れるのか?」みたいな部分が事前に見つけ出される。これは、モジュールの設計がTEAによる具体的な制約を受けているからだ。制約の具体性と予測可能性(Predictability)は比例の関係であり、これはElmのTEAに限った話ではない*2

また、先回りしてモジュール設計をレビュワーやメンバと握っておくことで「このモジュールはなんのための存在か?」みたいな実装の大前提になるような知識を共有できる。地味にこの部分の齟齬がでかいとレビューで時間を食ったり、アプリケーション全体でモジュール設計の方針がめちゃくちゃになっていったりする。

小さい機能であればここまでやらなくてもいいが、新しく大きな機能開発でモジュールを作ったりする場合にはこれくらいやっておくと工期や品質の安定感が違う。

*1:とはいえ、単なる定数的な型とか関数のあつまりにしかならないようなモジュールが生まれることは現実世界ではあるっちゃあり、ここはベストエフォートでって感じ。こういうモジュールもちゃんと仕様を分析すれば適切なモジュールに統合して凝集性を高められたりするが、仕事でやるとなると工数的にやっていられない場合もある。

*2:たとえばクリーン・アーキテクチャもFluxアーキテクチャも、わざわざアプリケーション設計に制約を与えることでモジュールの構成や責務を予測しやすくしている。