ドメイン駆動Vuexで複雑さに立ち向かう
スタディスト開発部、フロントエンド担当の小宮山です。走ることが楽しくなりすぎてフルマラソン完走が当面の目標です。
今回は私達が進めているUIリニューアルプロジェクトにおける、フロントエンド設計の心臓部についてご紹介したいと思います。盛り上がりつつあるものの、まだまだ実践的な情報が少ないVue界隈に少しでも貢献できましたら幸いです。
画面駆動Vuexの頃
プロジェクト始動当時は私含め大規模プロダクトにVuex(さらにその他Flux状態管理も)を導入して開発を進める経験も知見もほぼない状況でした。
そして開発していく画面デザインの大枠は出揃っている状態だったので、計画も実装も画面単位で区切って進みだしていきます。
こうした状況でVuexのstoreはどのような方針で実装されていくか。正確に表現するなら、特に方針なく実装していくとどうなるか。画面ファーストで、画面から使いやすく、画面ごとに専用なstore、画面駆動Vuexが自然と形成されていきます。
フロントエンド全面リニューアルプロジェクトスタート
画面駆動で作るVuexにも様々なメリットがあります。先に述べた私達のようなプロジェクト状況では、画面駆動でのstore実装は分かりやすく理に適った手法ではないでしょうか。
実際に私達も特に意識していたわけではないですが、画面駆動でstore実装を進めだしました。そして初めてのVueとVuexに苦戦しつつも、1つの画面をそれらしく動くところまで実装することができました。
イメージやデザインでしかなかったものが、この手で操作できる形に成った瞬間の喜びは説明するまでもないでしょう。フロントエンドに携わるエンジニアならなおさらです。
綻び
自分で作った画面をニヤニヤしながら繰り返し触っている時間はそれほど長続きしませんでした。なぜなら実際に1つの画面実装を行ってみて、storeを場当たり的に、画面駆動で作っていくことの危うさが早くも見えてきたからです。
Fluxの加護を受けたグローバル変数
データをDOMから開放して一元管理しようというFluxの思想は素晴らしいものです。しかし語弊を恐れずに言うなら、結局のところstateとはグローバル変数で、storeとはそのグローバル変数の集積所です。
さらにVuexはプラグインとしてインストールされるため、全てのコンポーネントは等しくstoreにアクセスする権利を得ます。
どこからでも簡単にアクセスできるグローバル変数がある。Fluxの一元管理思想がその利用を後押しする。結果として、状態に関わりそうな情報は全てstoreに置かれていきます。
確かに一元化には成功しています。しかし無計画に作られていくグローバル変数が一体何をもたらすのか、その身を持って体験せずとも歴史に学ぶだけで十分でしょう。
スケールするほど大きくなるノイズ
画面駆動でstoreを作っていくので当然画面ごとにmodule(storeを区分けして整理できるネームスペースのようなもの)が必要です。
本プロジェクトの画面デザインカンプ数は約80、多少の重複を考慮して50画面程でしょうか。つまりmoduleが50個必要です。
似ている画面はmoduleを使い回せば手間が減るかもしれません。保守性向上のためにも共通化は積極的に行っていきたいところです。
しかし思い出してください。moduleは先に述べたように、Fluxの名のもとに画面から押しやられた情報で溢れています。そこへさらに複数画面が好き勝手に入り乱れてくる状況はとても想像したいものではありません。
開発スケールが大きくなればなるほど共通化がもたらす恩恵と重要性も大きくなるのですが、グローバル変数置き場と化したstoreは共通化の障害となるノイズが多すぎます。
勇気
画面駆動で作ってきたそれまでのstore実装、全てを捨てて設計に立ち戻りました。
ドメイン駆動Vuex
フレキシブルドメイン駆動設計
予め補足しておくと、ドメイン駆動設計(DDD)は本の厚さからも分かるようにものすごく深く、私も全容を把握しているとはとても言えません。そして実際にその思想全てをフロントエンドの世界に当てはめる必要もないと考えています。
初登場は20年近くも前で、言及されているのはシステムのコアとなるサーバーサイドが大部分です。フロントエンド特有の制約を全てドメイン駆動の文脈で解決しようとするのはなかなかに無理があります。有用な思想は取り入れ、合わないと思ったものは無理に取り入れない柔軟なドメイン駆動設計に私たちは取り組んでいます。
ドメインデータと画面データ
storeのグローバル変数化問題とノイズ問題の根本にある原因は、storeに格納すべきデータが明示されていないことです。それではstoreに格納すべきデータとはなんでしょうか?これには様々な意見があると思われますが、本プロジェクトではドメイン駆動設計を設計思想として採用しました。
ドメインとなるデータをstoreに格納し、ドメインではないデータ、例えば画面由来の一時的なデータはコンポーネント側のローカルstateに格納します。
store設計については、Reduxの文脈でも本質は同様なことが多いです。Vuexに限定してしまうとまだまだ情報は限られてしまうので、Fluxな情報を手広く収集することもおすすめです。特にstoreにおけるドメイン vs 画面の考えを知るきっかけとなったこちらの記事は非常に参考になりました。
fluxもReduxもどうデータを持つかについてはあまり多く説明されてない気がします。もちろんアプリケーションによって持ち方は異なるとは思うんですが、自分用のメモとして改めて考え方を整理しておきたいと思います。 ...takayukii.me
ドメインの複雑さをstoreに押し留める
storeにドメイン由来データを格納すること自体は、画面駆動な設計においても特に不自然なことではないと思われます。明確に異なるのは、非ドメインなデータをstoreから排除したことです。ドメイン駆動設計の言葉を使うなら、ドメインをレイヤーとして分離したことになります。
この分離はコンポーネント側(UIレイヤー)にとても大きな恩恵をもたらしました。なぜなら、ドメインがstoreに分離されたことで、ドメインの複雑さがコンポーネント側に漏れ出すことが無くなったからです。
ドメインのデータはどうしても複雑になりがちです。特にレガシーな実装をリプレースする今回のようなプロジェクトでは、バックエンドのデータ構造の複雑さがそのままAPIからフロントエンドに伝えられていることが多々あります。
そして残念なことに、その複雑さは解決されないまま 持ち越され続け、最終レイヤーであるUI層で全てのツケを解決することが強いられます。
MVC、MVVM、Fluxやその他にも様々な設計パターンが提唱されていますが、UI層にビジネスロジックを書けと推奨するものは皆無ではないでしょうか。ドメイン駆動設計においても、このような実装は「利口なUI」としてアンチパターン認定されています。
UI層の仕事はドメインの複雑さを解決することなどではありません。その名の通り、ユーザーとのインタフェースを築く豊かな表現に集中すべきです。つまりドメインの複雑さをUI層から分離・隔離し、UI層が本来の仕事に集中できる洗練されたモデルを提供する存在が必要です。その存在こそがstoreです。
ドメインの複雑さを全てstoreで吸収する、これが本記事で紹介するドメイン駆動Vuexの核心です。
実践Vuex
Vuexは4つの大きな機能、action、mutation、state、getterから構成されています。そしてこの4つの機能が、ドメイン駆動設計なstoreを実現するための見事な連携を実現してくれます。
action
APIにリクエストを行い、レスポンスとして受け取ったデータをmutationに受け渡します。
APIは必ずしもコンポーネント側から使いやすくなっているわけではないので、ドメインの複雑さと同様に、その使いにくさをactionで吸収します。
例えば、コンポーネント側ではワンアクションで何かの情報を更新する処理を行いたいのに、複数のAPIにリクエストをしなければならないというような場合、複数のAPIにリクエストする処理を1つのactionにまとめてしまえば、コンポーネント側はAPIの面倒な実装を気にする必要がなくなります。(パフォーマンスや異常系のことを考えるなら、処理を統一するようなAPIを用意するのがベストだとは思います。)
ただし注意して欲しいのは、コンポーネントはAPIの面倒臭さを気にする必要がなくても、我々開発者はたとえコンポーネント側を実装しているときでも、その面倒臭さを意識していなければいけません。
ドメイン駆動設計においても同様のことが言及されています。ドメインの複雑さがうまく隠蔽されているとしても、開発者は常にドメインを意識して注意を向けていなければなりません。
コンポーネントとAPIでキャメルケースとスネークケースが食い違っている場合も、actionで吸収してしまえばコンポーネント側にスネークケースが溢れることが無くなります。レスポンスについても後述のgetterで吸収してしまいしょう。
mutation
stateを変更できる唯一の存在であることはVuexの仕組み通りです。ただし、コンポーネントからの直接アクセスはルールとして禁止します。
非同期な処理が必要ない場合でもactionを経由することは一見冗長ですが、実はstoreにドメインデータしか格納されていない場合、バックエンドにアクセスせずしてstateに変更を加えることは非常に稀です。よほど特殊な事情でも無い限りコンポーネントからのアクセスは禁止し、データ変更の受け口はactionに限定してしまって問題ありません。
state
APIから取得した生のデータを格納します。Vuexの仕組みとしてはコンポーネントからもアクセスできますが、ドメインの複雑さを封じ込めるために、コンポーネントからstateへの直接アクセスはルールとして禁止します。
getter
stateに格納されたデータをドメインとして解決し、モデルに変換してコンポーネントへと提供します。コンポーネントはstateへのアクセスを禁止されているため、常にgetterから変換済みのシンプルなモデルを取得することができます。
変換結果はVuexの仕組みによってキャッシュされるので、変換を通すことによるパフォーマンス低下を心配する必要はあまりないでしょう。
モデル
Vuexの構成要素ではありませんが、getterが生成するモデルはこの設計において非常に重要な要素の1つです。ドメイン駆動設計の文脈におけるモデルと大体同じなのですが、コンポーネント側にとって理解しやすいというフロントエンド特有の要件が求められます。
例えばUIの用語とバックエンドの用語に不一致が生じている場合(歴史あるプロダクトではよくある状況だと思います)、モデルはUIの用語で定義します。本来のドメイン駆動からは逸脱するかもしれませんが、ドメイン由来の複雑さはstoreは隔離するという方針の元、私たちはこのようなモデル設計を行っています。
コンポーネントが利用するモデルを明確に定義したことは開発効率化に大いに貢献しました。コンポーネント実装時にどのようなデータがどこから取得できるのか調べる手間や、実装者による用語のばらつきをかなり抑えることができています。
また本プロジェクトでは採用を見送ってしまいましたが、モデルはTypeScriptやFlowなどによる型定義と非常に相性が良いはずです。
画面駆動よりも画面ファースト
意外に思えるかもしれませんが、上述のようなstore設計はかなり画面ファーストな設計となります。なぜならコンポーネントとstore間のインタフェースとなるactionとgetterは、コンポーネントにとって使いやすくなっていることが重視されるからです。実際この設計をベースとしている私達のプロジェクトでは、コンポーネント実装は快適に、かなりのハイペースで実装が進みました。
もちろんコンポーネント側から追い出したドメインの複雑さをstore側で何とかするのも自分たちの仕事なので、決して複雑さが無くなるというわけではありません。あくまでもコンポーネントはUIとしての表現に、storeはドメイン由来の複雑さにそれぞれ集中するという役割分担を行っているだけです。たったそれだけの役割分担ですが、ちょっとした整理整頓で世界の捉え方は大きく変わるものです。
結論
ドメインの複雑さに真正面から立ち向かう
再掲になりますが、ドメインの複雑さを全てstoreで吸収することこそが本記事で紹介したドメイン駆動Vuexの核心です。結局のところそれだけのことなのですが、ドメインをstoreに隔離したことは、その複雑さに真正面から立ち向かう舞台と仕組みを私たちに与えてくれました。ここまで揃ったのならあと必要なのは勇気だけです。
道具の使い方は説明書に書いてある
多くの先人達の努力と犠牲が積み重なり、正しく叡智と称せるような素晴らしいフレームワーク達がフロントエンド界に登場しています。しかしそれがどんなに素晴らしい機能を我々に提供してくれるとしても、フレームワークを使うことが、設計を放棄することの免罪符とは成り得ません。
フレームワーク選定は間違いなくプロジェクトにおける重要な選択で、それには十分な時間を割くべきでしょう。ただし選んで終わりではありません。そのフレームワークを取り入れて、目指すプロダクトを設計することこそが真に重要です。設計プロセス無くして、道具に溺れた古き良きフロントエンド開発を脱することは難しいでしょう。
ビジネスと仲良く
エンジニアサイドの綺麗事だけで片付けることができない例外があるとしたら、それはビジネスサイドからの要請です。特にスタートアップのように、プロダクトをとにかく早くリリースすることに価値が置かれる場合です。
ここまで散々ドメイン駆動な設計を推してきましたが、小規模なものをスピーディに作るのであれば、少し言葉が悪いですが、画面駆動に行き当たりばったりに作ってリリースしたほうが良い場合も多々あるかもしれません。
最後に
スタディストでは、マニュアル作成・共有プラットフォーム Teachme Biz のドメインに真正面から一緒に立ち向かってくれる仲間を探しています。立ち向かう問題は複雑であればあるほど燃えてくる、そんな気迫をお持ちの方からのお声掛けを私たちはいつでもお待ちしております。