Vue.js を利用すると仮定して、だいたいこんな感じに登場人物を分けるといいんじゃないかというメモ。View フレームワークのコンポーネントの分類の話ではなくて、コンポーネントに限らずに単純に責務を分離しようとしているだけ。
考え方の基本は以下の2点。
- 並行作業のしやすさ(クリティカルパスの短縮)
- テスト容易性の確保
以下、仮に UI を決める component の名前を UI.vue とする。なお、☆の数は現時点で考えている重要度を表す。
☆☆☆ Model.jsとの分離
データによって UI に変化が起きるデータバインディングの部分は「データ取得の仕組み」に依存してはいけない。
- View が Vue コンポーネントから data をセットされる際、その仕組みの詳細に依存する必要はない
- YAML を直接読み込んでいようが API にアクセスしていようが View には影響しない
- つまり Vue コンポーネントの中にその仕組みの詳細を置いてはいけない
- 置いてあったら詳細の変更の影響を受ける
上の考え方を適用すれば「View」と「データ取得、反映の仕組み」の開発は分離して同時に行えるのでお互いの作業をブロックしないし、それぞれ自動テストに対応させやすい。
もちろん、設計段階でお互いに連携しなければいけない責務は 考慮はしておく。例えばデータの取得に失敗した場合のユーザーへのフィードバックの方法などは Model でエラーの種類などを整理したうえで View にどうやって反映するかを考えておかなければいけない。
この辺をまず大雑把に Model と呼び、View component から分離しておく。この考え方で言う Model は特定のフレームワークに依存する必要はないので VanillaJS で書いておけばよい。attribute の扱いやエラーの扱いなどの共通の処理を内包するなんらかのライブラリがあるならそれを使ってもよいかもしれないが、Model すなわち State を持つ Store というわけではない。ここ重要。
☆☆ Container.vueとの分離
Container と Presentational の分離の話。methods, data, props, $emit の話。
例えば分離しておけば Model を仮置きしておいて後で差し替えるといった際に UI.vue 側は何も変更する必要がない。
cf. Vueで雑にContainer Component - あーありがち(2019-02-03)
☆☆ Error.vueとの分離
- エラー処理をいちいち if で分岐すると処理の本質を追いにくくなる
- 例えば Error.vue を用意し <Error v-if="errors.length > 0" :errors=errors> で丸投げ
ViewModel から見て model.errors と this.errors の 2パターン用意できるかも。
※ ActiveModel::Errors の考え方を転用。
☆ ModelとModelState.jsの分離
- Model と ModelState は分けてよい
- Flux 系 Store は State だけ担っていると考えればよい
☆ ModelとInteractor.jsの分離
Clean Architecture 的な意味での Interactor みたいな何か。Model 一つで処理できなければ、複数の責務にまたがる一つの処理にまとめる人を用意する。
errors も relay していく感じ。
まとめ
こんな構造になるかな。
Container.vue (3)
`-- UI.vue (1)
| `-- Error.vue
`-- Model.js (2)
| `-- errors
`-- ModelState.js ( nearly equal Store ) (4)
順番としては 1, 2, 3, 4 で考えたらいいんじゃないかと考えている。Interactor を入れるとこんな感じかな?
Container.vue (3)
`-- UI.vue (1)
| `-- Error.vue
`-- Interactor.js (5)
| `-- ModelA.js (2)
| | `-- errors
| `-- ModelB.js
`-- ModelState.js ( nearly equal Store ) (4)
書いてみて思ったんだけど、こういう考え方だから、世間でよく観測される ViewModel コンポーネントの分類の話や Model == Store みたいな話にピンとこないんだなぁということが分かってきた。