実況中継シリーズ - 複雑なJavaScriptアプリケーションに立ち向かうためのアーキテクチャ

先日行われた builderscon tokyo 2017 にて、「複雑なJavaScriptアプリケーションに立ち向かうためのアーキテクチャ」という発表をしてきました。この記事では、そのプレゼンテーションの再現を行います。

アバンパート

f:id:nkgt_chkonk:20170808142626p:plain

本日はこういう発表をします。よろしくおねがいします。

f:id:nkgt_chkonk:20170808142630p:plain

普段はメディロムという会社で働いていて、ScalaとJSを主軸に活動しています。PerlとRubyもたしなむ程度には書きます。業務では、業務で使うアプリケーションをブラウザプラットフォーム上に作っています。こういうブログも書いてるんでよかったら読んでください。

f:id:nkgt_chkonk:20170808142634p:plain

あと、何度かweb+DBプレスに特集書かせてもらっていて、とくに左の「データ構造の基礎知識」ってやつは自分で言うけどまじでいい記事なんでまだ読んでないひとはバックナンバー買って読んでください。

f:id:nkgt_chkonk:20170808142640p:plain

さて、複雑なアプリケーションに立ち向かうためのアークテクチャについてです。まずは、複雑なアプリケーションに対して何も考えずに立ち向かうとどういう悪いことが起こるのかを見ていきましょう。

f:id:nkgt_chkonk:20170808142644p:plain

アーキテクチャとか設計の話って抽象的なこと言っててもポエミイに感じちゃうんで、実際の例題をもとに考えていきましょう。この例題はじつは去年のbulidersconでも出したんですけど、復習がてら、例題として、画像ピッカーを考えてみましょう。

f:id:nkgt_chkonk:20170808142648p:plain

「選択された画像」と「最近投稿された画像」ってのがあって、最近投稿された画像をクリックすると「選択された」という状態になって、「選択された画像」をクリックすると非選択状態になるみたいなやつです(トークではここは動画でした)。

f:id:nkgt_chkonk:20170808142654p:plain

HTMLはこんな感じになります。「最近投稿された画像」はサーバーから取ってきて動的に表示するので、空にしといてあとからいじることにします。もちろん「選択された画像」も動的な要素なので、空にしておきます。

f:id:nkgt_chkonk:20170808142659p:plain

JSはこんな感じ。すみません細かくて見えないと思うんですけど、ひとつのclassでできています。細かいところを見ていきましょう。

f:id:nkgt_chkonk:20170808142703p:plain

コンストラクタはこんな感じで、jQueryのエレメント、っていうんですかね、エレメントを保持しておきます。

f:id:nkgt_chkonk:20170808142708p:plain

最近投稿された画像を表示する部分がここですね。念のため保持したエレメントを空にしたあと、HTTPリクエストを飛ばして、画像の一覧を取得したらそれをもとにDOMを作ってappendしていっています。

f:id:nkgt_chkonk:20170808142712p:plain

で、クリックで画像を選択する部分のコードがこんな感じですね。クリックされたエレメントとおなじURLを指すimgがすでに選択されてたらなにもしない、選択されてなかったら新しくDOMにappendするって感じのコードですね。このDOMをクリックされた場合は非選択状態にしないといけない、つまりDOM上から削除しないといけないので、そのためのイベントとハンドラもここで設定しています。

f:id:nkgt_chkonk:20170808142719p:plain

これでも動くんですけど、ちょっと嫌な予感がしますね

f:id:nkgt_chkonk:20170808142724p:plain

まずここを見てください。サーバーから取ってきたJSONをもとに、DOMをここで作ってます。これなにが臭うかというと、ちょっとデザインを変更したいねって場合を考えてみてください。ただHTMLの構造変えたいだけなのに、JSのロジックを見に来ないといけませんね。

f:id:nkgt_chkonk:20170808142730p:plain

次はここに注目してみます。最近投稿された画像一覧を出すときに、イベントとハンドラを設定していますね。

f:id:nkgt_chkonk:20170808142737p:plain

選択された画像を表示するところでも同様です。嫌な予感がしますね。もし、このアプリケーションがぶっ壊れて、「なんかここクリックしたら意図してない動きをした」みたいなとき、イベントハンドラを探しにJSのロジックの森を探索しないといけなくなります。

f:id:nkgt_chkonk:20170808142747p:plain

あと怖いのはここですね。クリックされた画像がすでに選択されてるかどうか、DOMのsrcを見ることで判断しています。要するに、これってDOM上にしかアプリケーションの状態が保存されていないってことなんです。たとえばここから「選択された画像のURL一覧をAPIに投げて永続化したい」みたいなことが起こったら、DOMをさらってURLを集めないといけません。デザイン変更してDOM構造が変わってバグる未来が想像できますね。

f:id:nkgt_chkonk:20170808142753p:plain

まあこんな感じで、ある程度以上に大きいアプリケーションを作るときに雑にやっちゃうと大変悪いことが起こるわけです。

f:id:nkgt_chkonk:20170808142759p:plain

とはいえ、問題が小さいうちはこれでいいんです。十分に単純なアプリケーションならば、下手に分割するよりも全部ベターって書いてしまったほうが、ひとつのファイルやクラス、関数を読むだけで全貌を把握できるので、そのほうが見通しが良いでしょう。

f:id:nkgt_chkonk:20170808142806p:plain

しかし、アプリケーションが巨大になってくると、さすがにひと目で全部把握することができず、どんどん見通しが悪くなってくるわけですね。

f:id:nkgt_chkonk:20170808142811p:plain

複雑なアプリケーションに立ち向かうために、なんとかして問題を分割して見通しを良くしたいなあ、というのが、私たちが今回スコープとする問題意識です(逆に言うと、小さい問題に関しては今回はスコープとしません)。

f:id:nkgt_chkonk:20170808142817p:plain

さて、分割すると言っても、どのように分割すればいいのでしょうか?いろんな分割の方法が考えられますが、この分割の仕方を考えるのが設計だし、その結果としてアーキテクチャが描き出されます。どのように分割するかの指針ですね。

f:id:nkgt_chkonk:20170808142822p:plain

大切なことなんで再度言いますが、十分に単純なアプリケーションを無駄に分割する必要はありません。そんなことしても無駄で見通しが悪くなるだけです。そうするとFizzBuzz Enterprise Editionみたいになっちゃいますからね。

f:id:nkgt_chkonk:20170808142827p:plain

繰り返しになりますが、「複雑なアプリケーションを」どう分割すればいいのか?そこになんらかの指針はないのか?というのが、我々の関心ごとです。

f:id:nkgt_chkonk:20170808142834p:plain

こまったときには先人の知恵を紐解いてみましょう。「こういう視点にたってアプリケーションを分割するといいよ」という先人の知恵はたくさんあります。本日はこれらの知恵について概要をまとめ、コードを交えて実際の例を見ていきましょう。

f:id:nkgt_chkonk:20170808142838p:plain

アニメで言えば、ここでアバンパートは終了です。アバンパートが終了したら何があるか?そうです、オープニングが流れますね。残念ながらオープニング曲は存在しないので、ここで息子の写真を紹介しましょう(会場ではここでめちゃめちゃかわいいわたしの息子の写真が大写しになりました。これなかったひとは残念でしたね!!!)

Aパート(1) - PDS

f:id:nkgt_chkonk:20170808142844p:plain

というわけでAパートです。

f:id:nkgt_chkonk:20170808142849p:plain

さきほどの先人の知恵を再掲します。

f:id:nkgt_chkonk:20170808142858p:plain

まずはPDSから見ていきましょう。

f:id:nkgt_chkonk:20170808142901p:plain

PDSは、ファウラー先生がblikiに書いているのでそれを読んでみてください。簡単に言うと、UIや入出力に関わるものとそれ以外でアプリケーションを分割しましょうね。という話です。

f:id:nkgt_chkonk:20170808142917p:plain

さっき見た、巨大で見通しの悪いアプリケーションの図をPDSに則って分割すると、このようになりますね。DOMやCSSなどの見た目や、ユーザーからの入力など、プレゼンテーションが左側に、それ以外の問題が右側に分割されました。

f:id:nkgt_chkonk:20170808142923p:plain

では、問題をこのように分割するためには、実際にはどのように実装を進めればいいのでしょうか。

f:id:nkgt_chkonk:20170808142931p:plain

PDSを実現するための実装パターンとしてよく知られているのが、MVWhateverと呼ばれるものです。MVCやMVP、MVVMとか言われるやつですね。あとReactもPDSを実現するための実装パターンですが、今回はMVVMを例にとって、さきほどのカオスな画像pickerを改善していきましょう。

f:id:nkgt_chkonk:20170808142937p:plain

MVVMでは、Presentation層をViewとViewModelが、Domain層をModelが担当します。

f:id:nkgt_chkonk:20170808142942p:plain

ViewにはDOM定義とCSS定義、イベント定義を書きます。

f:id:nkgt_chkonk:20170808142948p:plain

実際のViewはこのような感じで書きます。以前は空だったdivタグの中に、最終的にレンダリングされるべきDOM構造がきちんと定義されていますね。jQueryのときはロジックでDOMを組み立てていましたが、MVVMのViewに責務を整理することでDOM構造はここ見ればわかるようになりました。

また、@clickという見慣れないプロパティがあって、そこでunselectという関数を呼び出すみたいなことが書かれていますね。これがイベントの定義です。あと、{{i.url}}とか書かれてますが、これは動的に値をレンダリングしてる部分ですね。(Vue.js的には本来あまりよい書き方ではないのですがちょっと説明上こういう書き方で)

f:id:nkgt_chkonk:20170808142954p:plain

そして、ViewModelが、Viewに対してデータを流し込みます。後述しますが、Vue.jsではこのデータ流し込みをデータバインディングと呼ばれる機構を利用して行います。また、Viewで定義されたイベントを受け取るのも、ViewModelのしごとです。

f:id:nkgt_chkonk:20170808142958p:plain

では、データバインディングとはどういうものなのかを見ていきましょう。まず、ViewModelはViewのための状態ストアを持ちます。 で、この状態ストアを書き換えると、勝手にそれと紐付いたViewも書き換わる、というのがデータバインディングです。

f:id:nkgt_chkonk:20170808143004p:plain

さて、Vue.jsにおいては、VueというクラスのインスタンスがVMとして振る舞いますが、その中のdataというプロパティが、Viewのための状態ストアを表します。 今回ならば、「選択された画像」と「最近投稿された画像」が動的な要素なので、これらを状態ストアにぶち込んでおきましょう。初期値は空の配列です。

f:id:nkgt_chkonk:20170808143009p:plain

前述のとおり、View側では特殊な記法をつかってこのデータを読み出しています。

f:id:nkgt_chkonk:20170808143014p:plain

ちょっとためしに、5秒後にViewModelのdataを書き換えるみたいなやつをここにかいてあるんですけど、これを実行するとこんな感じになります。

f:id:nkgt_chkonk:20170808143019p:plain

(実際のトークではここは動画でした)

f:id:nkgt_chkonk:20170808143023p:plain

さて、ViewModelには、Viewからのイベントを待ち受けてそれをハンドリングする責務もありました。

f:id:nkgt_chkonk:20170808143028p:plain

Vue.jsの場合、Viewでは”@click”みたいなアトリビュートでイベントを定義できるんですけど、ここに関数名書いておくと、VMの”methods”っていうキーで表されるオブジェクトに定義した関数が呼び出されます。たとえばこの上のほうのimageをclickすると、

f:id:nkgt_chkonk:20170808143033p:plain

ここのmethods.selectが呼び出されるという塩梅ですね。Viewで発生したイベントをViewModelで受け取って、ViewModelが保持してるModelのメソッドにdispatchしています。

f:id:nkgt_chkonk:20170808143037p:plain

ここで重要なのは、ViewModelにはロジックが書かれてなくて、Modleのメソッド呼んでるだけってことです。ここにロジックとか状態の変更とかいろいろ書いちゃうと、せっかくPDSを実現しようとしたのにViewModelっていうプレゼンテーション側の部品にプレゼンテーション以外のものが紛れ込んできちゃうわけですね。ここになんか複雑なこと書かれてたらPDSが破綻してるシグナルです。気をつけましょう。

f:id:nkgt_chkonk:20170808143046p:plain

さて、改めてMVVMを図にして考えてみましょう。まず、Viewでイベントが発生します。

f:id:nkgt_chkonk:20170808143055p:plain

で、Viewモデルがそのイベントに応じて、Modelのどんなメソッドを叩くか決定します。

f:id:nkgt_chkonk:20170808143102p:plain

ModelのメソッドがModelの内容を書き換えます。

f:id:nkgt_chkonk:20170808143106p:plain

さて、例の図で考えてみると、このタイミングで、Modelの中にある状態が変化したわけです。この変化を、どうにかしてUIに書き戻してあげる必要があるでしょう。

f:id:nkgt_chkonk:20170808143110p:plain

何かが変化したのを監視してなにかする!オブザーバーパターンですね!と、あえてここではわざとらしくオブザーバーパターンを利用しましたが、ModelがViewModelを保持して、ModelがViewModelを直接変更してはいけないのでしょうか。それがあまり良い手法ではない理由は後述します。

f:id:nkgt_chkonk:20170808143117p:plain

さて、というわけで、オブザーバーパターンを利用して、ViewModel側では、Modelが発生させるイベントを監視して、Modelの変更を感知したらModelから値を読みだして自身の状態Storeを書き換えてあげましょう。これで、データバインドの仕組みを通じてUIも書きかわるわけですね。めでたい。

f:id:nkgt_chkonk:20170808143124p:plain

ViewModelとModelの関係を図にするとこういう感じですね。

f:id:nkgt_chkonk:20170808143129p:plain

まとめるとこういう感じです。MVVMパターンでは、Viewにて発生したユーザーイベントをViewModelが受け取ってViewModelはModelのメソッドをdispatchします。Modelの変化をViewModelがObserveして、その変化をdatabindを通じてViewに反映させます。

さて、このときModelからViewModelに対してNotifyの矢印が出ていますが、ModelはViewModelを保持していない、つまりModelがViewModelに依存していないことに気をつけてください。

ViewModelやViewは、DOMが絡んでくるので、非常にテストしにくい部品です。Modelがそういうテストしにくい部品に依存しないようにすれば、「アプリケーションの論理的な挙動」がテストしやすいし、綺麗にすっきりかけそうなきがしてきませんか。そのために、オブザーバーパターンを利用してドメイン層からプレゼンテーション層への依存を断ち切っているわけです。

余談ですが、このPとDの間をDIとInterfaceを利用して分離するのがMVPなわけですね。

さて、これで、「プレゼンテーション層」と「ドメイン層」の分離を実現できました。

f:id:nkgt_chkonk:20170808143134p:plain

PDSを実現する前は、このようないろいろな問題がありました。

f:id:nkgt_chkonk:20170808143144p:plain

PDSを実現したことで、複雑な問題をひとまずプレゼンテーションとドメインに分割でき、少し見通しがよくなったように思います。

f:id:nkgt_chkonk:20170808143149p:plain

ところで、MVVMとかMVPとかそういう話ですが、わたしは「それがMVVMなのかMVCなのかMVPなのか」とかそういうことにはあまりこだわらなくてもいいんじゃないかなと思います。そもそもの目的は、複雑な問題領域を分割統治して考えたいという目的であって、その目的を達成するための実装パターンとしてMVVMやMVPがあるわけです。「時代はMVVM」とか「時代はMVP」とか考えないで、プラットフォームとかフレームワークにとって適切な方法でPDSを実現することが肝心だと思います。あくまでMVWはそのための「パターン」ですからね。これらを参考にしながら、よりよいPDSを実現していけばいいわけです。唯一の正解なんてないです。TIMTOWTDIです。

f:id:nkgt_chkonk:20170808143154p:plain

さて、PDSを実現するMVVMによって、プレゼンテーション側は整理できました。ここで、ドメイン側、言葉を帰ればMVWのModel側について見てみましょう。

f:id:nkgt_chkonk:20170808143159p:plain

改めていつもの図を見てみると、ViewやViewModelの役割は結構はっきりとわかりましたが、

f:id:nkgt_chkonk:20170808143203p:plain

Modelの部分はまだまだいろいろな責務があり、混沌としているように見えます。

f:id:nkgt_chkonk:20170808143212p:plain

それもそのはずで、そもそもPDSというのはプレゼンテーションと「その他」を分けるという考え方です。

f:id:nkgt_chkonk:20170808143217p:plain

それを考えたら、MVWhateverというのはあくまで「どうやってプレゼンテーションを分離するか」ということについては語ってくれても、「どうやってモデルを書くのか」についてはなにも語ってくれないわけです。

f:id:nkgt_chkonk:20170808143222p:plain

そこで、「じゃあモデル側はどうするんですか」というときの指針の「一例」として、レイヤードアーキテクチャを導入してみましょう。

Aパート(2) - レイヤードアーキテクチャ

f:id:nkgt_chkonk:20170808143226p:plain

レイヤードアーキテクチャは「モデル層も含めて、いろんな層に分けましょう」という考え方で、よくあるのはこういう分け方です(これが唯一の正解というわけではありません)。

f:id:nkgt_chkonk:20170808143231p:plain

それを考えると、レイヤードアーキテクチャとMVWやPDSは決して排他的なものではなく、むしろ相補的なものです。今まで出てきた概念を図にすると、こんな感じになります。まず、PDSは、プレゼンテーションとドメインを分けよう、という考え方でした。MVVMを利用すると、ViewとViewModelでプレゼンテーション層を実現して、そのほかは全てモデルに書く、という形でPDSが実現できたわけですね。Layered Architectureは、さらにそのモデルの部分を「Application, Domain, Infrasutructure」に分けているわけです。

ここで、Domainという言葉が2つの異なる意味で使われていることに気をつけてください。PDSの文脈でも、LayeredArchitectureの文脈でも、Domainということばが使われていますが、これらは文脈によって指しているものが異なります。気をつけてください。

f:id:nkgt_chkonk:20170808143235p:plain

さて、設計やアーキテクチャの話は概念だけ説明してもよくわからないので、やはり例題を出しましょう。さっきは画像ピッカーを例題としましたが、LayeredArchitectureの例題はドラムシーケンサーにします。これは実際にコードもGithub上に上がっているので、是非見てみてください。NekogataDrumSequencer(トークではここでデモ)

f:id:nkgt_chkonk:20170808143239p:plain

さて、LayeredArchitectureでは、しばしばモデル層を3層に分割します。アプリケーション層は、プレゼンテーション層から命令を受け取ってドメインとインフラを操作する層、ドメイン層はアプリケーションの論理的な挙動を定義する層、インフラストラクチャは実装依存なものを書く層です。例をあげるとfetchAPIでサーバーとやり取りするとかそういう部分ですね

f:id:nkgt_chkonk:20170808143244p:plain

まずはApplication層から見ていきましょう。 さっきも言ったとおり、Application層はプレゼンテーション層の窓口になる層です。今回で言えばViewModelが直接こいつのメソッドを叩く、というわけです。この層は後述するDomain層やInfrasturucture層を呼び出すだけにとどめておくと良いでしょう。

f:id:nkgt_chkonk:20170808143248p:plain

ドラムシーケンサーでの実際の実装は抜粋ですがこんな感じです。

f:id:nkgt_chkonk:20170808143253p:plain

たとえば、ドラムシーケンサーには「トラックを選択する」というユースケースがありますよね。それをselectTrackというメソッドで表現しています。実際にやっていることは、sequencerというドメイン層のオブジェクトが持っている「現在のパターン」に対して、「トラックを選択」という命令を送っているだけです。実際の実装はここには書かれていません。

f:id:nkgt_chkonk:20170808143259p:plain

で、モデル層の状態が変わったら通知しないといけないんで、今回はここで通知をしています。

f:id:nkgt_chkonk:20170808143303p:plain

次はドメイン層を見ていきましょう。一番重要になるのがこのDomain層なんですけど、ここにアプリケーションの構造をモデリングしていきます。今回なら、Sequencerというのがあって、それが4つのPatternを持って、それらが選択可能で、PatternはSD,BDなどのトラックがあって、それぞれのトラックがScoreを持って、というのをそのまま書いていきます。

f:id:nkgt_chkonk:20170808143314p:plain

たとえばシーケンサーは、「パターンを4つ保持する」「パターンを選択することができる」という振る舞いをもっていますね。これをそのままこうやって書いていきます。

f:id:nkgt_chkonk:20170808143319p:plain

では、その「パターン」は、どういう振る舞いをするでしょう。パターンは4つのトラック(SD,BD,HH,RD)と、そのトラックに紐付いた16分音符16個のスコア(楽譜)からできています。また、パターンには初期パターンが存在します。(このへんの話はデモをみるか実際のプロダクトを触ってもらったほうがわかりやすいと思います)

f:id:nkgt_chkonk:20170808143325p:plain

また、楽譜については、書く音符について音符/休符がトグルできるようになってますね。

f:id:nkgt_chkonk:20170808143330p:plain

そして、これらがアプリケーション層から呼ばれることによって、ユースケースへの窓口をプレゼンテーション層に提供する形になります。

f:id:nkgt_chkonk:20170808143334p:plain

最後に、インフラストラクチャ層です。ここには、プラットフォーム依存なものとか実装依存なものを書いていきます。たとえば今回のドラムシーケンサーならば、「音を鳴らす」ためにはWebAudioAPIというブラウザに固有のAPIを叩かなければなりあません。あるいは、webAPIを叩くためにfetchAPIを利用する、みたいなものもブラウザ固有のAPIを叩くことになるわけで、インフラストラクチャ層に書かれることになります。

f:id:nkgt_chkonk:20170808143340p:plain

ドラムシーケンサーの実際のコードでも、インフラストラクチャ層でそういうコードを書いています。

f:id:nkgt_chkonk:20170808143346p:plain

「これってインフラストラクチャの関心?それともドメインの関心?」ってことを考えるときに、それがディレクターも興味を持つ話なのかそれともプログラマだけが興味を持つ話なのか、ということを考えてみるといいかもしれません。この考え方は要出典ですが。

f:id:nkgt_chkonk:20170808143352p:plain

さて、全ての層をだいたい見てきたところで、先程の図を再掲します。MVVMとLayeredArchitectureを導入したことで、問題はこのように分割されました。それをいつもの図にマッピングしてみましょう。

f:id:nkgt_chkonk:20170808143356p:plain

先ほどまではカオスだったモデル側が、ある程度きちんと分割できました。良いですね。

f:id:nkgt_chkonk:20170808143401p:plain

しかし、これでめでたしめでたしかというと、まだテスタビリティの問題があります。

f:id:nkgt_chkonk:20170808143406p:plain

愚直にレイヤードアーキテクチャで書くと、Application層やDomain層がInfrastrucutre層を利用するので、Application層やDomain層がInfrastructureに依存するようになります。

f:id:nkgt_chkonk:20170808143409p:plain

しかし、インフラストラクチャは環境依存のAPIなどを叩く層なので、とてもプラットフォーム/環境依存度が高い層です。

f:id:nkgt_chkonk:20170808143414p:plain

で、そういう層はテストがしにくいんですよね。たとえばfetchAPIが絡むテストやwebAudioAPIの絡むテストを考えてみてください。いかにもテストしにくそうですよね。また、テストしにくいものに依存したものはテストしにくいので、このままだとインフラストラクチャ層のせいで全ての層がとてもテストしにくいという問題にぶちあたります。

f:id:nkgt_chkonk:20170808143419p:plain

できたら、ドメイン層のような重要でなおかつプラットフォーム依存性の少ない層に依存する、という感じで、依存方向はこうしたいですよね。

f:id:nkgt_chkonk:20170808143423p:plain

そこで参考になるのが、クリーンアーキテクチャという考え方です。

Aパート(3) - クリーンアーキテクチャ

f:id:nkgt_chkonk:20170808143428p:plain

クリーンアーキテクチャというのは、様々なアーキテクチャパターンについて、依存の方向に着目したコンセプトです。原典はここです。

f:id:nkgt_chkonk:20170808143432p:plain

で、クリーンアーキテクチャが言ってるのは、つまるところ、「抽象度が高く、テスタビリティの高いドメイン層を中心にして、内側に対して依存するようにしましょう。外側に依存しないようにしましょう」ということです。原典に図が書いてあるので「この図の通りに実装するのがクリーンアーキテクチャだ」とよく誤解されがちなのですが、クリーンアーキテクチャ自体はなにか具体的な層を規定しているわけではありません。これは原典で言うと「Only Four Circles?」のところに書かれている内容ですね。

Only Four Circles?

No, the circles are schematic. You may find that you need more than just these four. There’s no rule that says you must always have just these four. However, The Dependency Rule always applies. Source code dependencies always point inwards. As you move inwards the level of abstraction increases. The outermost circle is low level concrete detail. As you move inwards the software grows more abstract, and encapsulates higher level policies. The inner most circle is the most general.

f:id:nkgt_chkonk:20170808143439p:plain

さて、それでは、クリーンアーキテクチャを実現するためにはどのような実装パターンが利用できるのでしょうか。そのうちのひとつとして、DIパターンが考えられます。そもそもDIパターン自体が依存性を逆転させるためのパターンなので、あたりまえと言えば当たり前ですね。DIについてよくわからないというひとは手前味噌ですが要するにDIってなんなのという記事を読んでください。

f:id:nkgt_chkonk:20170808143444p:plain

さて、今回のアプリケーションでは、ドメイン層やアプリケーション層からインフラストラクチャ層への依存を断ち切るためにDIパターンを利用しましたが、プレゼンテーション層とアプリケーション層の間に注目してみましょう。今回は、MVVMの考え方を導入し、オブザーバーパターンを利用することでアプリケーション層がプレゼンテーションに依存しなくて良いようにしたのでした。

f:id:nkgt_chkonk:20170808143449p:plain

図にしてみましょう。今回の事例を見てみると、プレゼンテーション - アプリケーション間の依存関係をオブザーバーパターンで整理し、(アプリケーション / ドメイン) - インフラストラクチャの間の依存関係をDIによって整理することで、「内側」に依存するルールを守ることができています。クリーンアーキテクチャの考え方をレイヤードアーキテクチャに適用することで、アプリケーションのテスタビリティをあげ、中心のドメイン層はUIやプラットフォームの都合にあまり振り回されずにアプリケーションの挙動をモデリングすることに注力することができました。めでたしめでたしです。

f:id:nkgt_chkonk:20170808143454p:plain

さて、これでめでたしめでたしかというと、そうでもありません。実際に大きなプロダクトを書いていると、状態に関する問題が立ち上がってきます。

f:id:nkgt_chkonk:20170808143501p:plain

今、ドラムシーケンサーでは、アプリケーションの状態はドメイン層に散らばっています。今回はこれで問題がないのですが、もう少し複雑な表示を持つようなアプリケーションの場合、更新操作はうまくドメインにマッチするけど、表示についてはそうではない、ということが起こりがちです。たとえば、なにかサマリーを表示するような画面を考えてみると、ひとつのドメインモデルに閉じるものではなく、むしろいろんなモデルからいろんな属性をちょいちょいと引っ張ってきたりとか、そういうことがままあります。

f:id:nkgt_chkonk:20170808143507p:plain

この問題に対応するために役立つ概念が、CQRSです。

Aパート(3) - CQRS

f:id:nkgt_chkonk:20170808143512p:plain

CQRSの基本的なコンセプトは「コマンド(更新操作)」と「クエリ(読み出し操作)」を分離しましょう、というコンセプトです。PDSが「プレゼンテーションとドメインを分離しましょう」だったことを思い出してください。CQRSは「さらにそこからコマンドとクエリを分けよう」って感じです。では、コマンドとクエリを実際に分ける際の実装パターンにはどんなものがあるのでしょうか。じつはこの辺りはたぶんまだ有名な実装パターンってあまりなくて、各位それぞれ工夫してるみたいな段階だと思います。

ReduxをこのCQRSの考え方から見てみたり、BFFパターンなんかはフロントエンド/バックエンドにまたがったCQRSの実装パターンとみることもできるかもしれません。

レイヤードアーキテクチャとの関連で見てみる場合、コマンド側は複雑なロジックを含むためドメイン層を含むレイヤードアーキテクチャを通じてState(状態)をインフラストラクチャ層に保存(このときRepositoryパターンと呼ばれる実装パターンを利用することが多いと思います)するという実装パターンがよく採用されるように感じます。

f:id:nkgt_chkonk:20170808143517p:plain

一方、クエリに関しては、状態を変更するときよりも自由に読み出したいことが多く、なおかつ複雑なロジックを経由したいことがそんなに多くないため、Stateを好き勝手読みだしてその値を加工する責務を持つ層をコマンドの層とは関係なく置くことが多いように思います。

f:id:nkgt_chkonk:20170808143521p:plain

それを図にするとこういう感じですね。もちろん、PDSが実装パターンではなくて「考え方」で、MVWが実装パターンであったように、CQRSも「考え方」であって、この層の通りに作るのがCQRSの唯一の正解というわけではありません。CQRSを実現するための実装パターンというのはいろいろ考えられるでしょう。

f:id:nkgt_chkonk:20170808143526p:plain

さて、今までの議論を例の図にマッピングするとこのようになります。だいぶ責務が整理されて、問題を把握しやすくなったのではないでしょうか。

f:id:nkgt_chkonk:20170808143532p:plain

Todoアプリを今までみたいな考え方で実装してみた参考実装がここにありますので、興味のある方は読んでみてください。これ参考実装とか言いながらScala.jsで書かれてて、まじかよって感じなんですけど、自分がアークテクチャの実験とScala.jsの実験一緒にやった結果なんでまああの、その、許してください。

f:id:nkgt_chkonk:20170808143538p:plain

というわけで、Aパートのまとめに入ります。

f:id:nkgt_chkonk:20170808143541p:plain

単純なアプリケーションならば無理に分割しないほうが見通しが良いですが、複雑な問題に立ち向かうためには適切に問題を分割する必要がありました。そして、その「適切な」とは一体なにか、というのがわたしたちの問題意識としてありました。

f:id:nkgt_chkonk:20170808143545p:plain

適切な分割方法について、先人たちはPDSやレイヤードアーキテクチャ、クリーンアーキテクチャ、CQRSなどの考え方を発明してくれました。それらはすべて「どのように分割するか」の指針です。この指針を実際に実装する際には、様々な実装パターンが参考になるでしょう。大事なのは、実装パターンに振り回されることではなく、「その実装パターンが解決しようとしている問題はなんなのか」を理解し、自分がその問題にぶちあったときに参考にすることだと思います。実装パターンに向き合わず、複雑な問題に向き合い、その武器として実装パターンを利用しましょう。

アイキャッチ、CM

さて、ここでAパートは終了です。Aパートが終了したらアニメだとアイキャッチが入りますね?ここでアイキャッチとしてめっちゃかわいいわたしの息子の写真をご覧ください(本番ではここで息子の写真が大写しになりました。本番に来られなかった方は見られなくて残念ですね!!!!)

f:id:nkgt_chkonk:20170808143555p:plain

Aパート、アイキャッチのあとはCMです。メンバー募集中です。

CM明け、Bパート前にもアイキャッチが入りますね?もちろんまた息子の写真です。かわいいですねえ(本番ではここでry

Bパート

f:id:nkgt_chkonk:20170808143603p:plain

続いてBパートです。

f:id:nkgt_chkonk:20170808143608p:plain

Aパートでは、概念の整理をしてきました。しかし、実際の業務で、これらを考えながら書いていてもちょくちょく「えーとこれはどこに書けばいいの?」ってことはあります。それは、CQRSまで考えないレベルのアプリケーションでもそうです。具体的な例をあげながら、それらを考えてみましょう。

f:id:nkgt_chkonk:20170808143618p:plain f:id:nkgt_chkonk:20170808143614p:plain

その前に、典型的なレイヤードアーキテクチャで使う層について復習しておきましょう。

f:id:nkgt_chkonk:20170808143623p:plain

プレゼンテーション層は、ViewやViewModelが置かれるのでした。これはまあフレームワークとかが提供してくれる層なんでこれでいいでしょう。

f:id:nkgt_chkonk:20170808143627p:plain

アプリケーション層には、よく「アプリケーションサービス」というのが書かれます。プレゼンテーション層から命令を受け取ってドメインやインフラをdispatchする君ですね。

f:id:nkgt_chkonk:20170808143633p:plain

で、ドメイン層なんですけど、これはアプリケーションが対象とする問題領域をモデリングする部分なんで、各自がんばってくれ!!!としか言えない部分です。

f:id:nkgt_chkonk:20170808143637p:plain

で、インフラストラクチャ層についてはプラットフォーム依存のものとかそういうのがここに書かれるのでしたね。

f:id:nkgt_chkonk:20170808143642p:plain f:id:nkgt_chkonk:20170808143646p:plain

現実は難しくて、「じゃあこれどこにどうやって書けばいいのよ」っていう話が結構出てくる、というのを見ていくのでした。

f:id:nkgt_chkonk:20170808143650p:plain

たとえば、今回は状態は全部サーバー側にあるので、Domainに書くModelもAPIのレスポンスから組み立てるみたいなことが起こります。

f:id:nkgt_chkonk:20170808143656p:plain

APIからはプレーンなJSONが返ってくるので、そこからどうやってDomainModelを組み立てるの?という話は出てくるでしょう。

f:id:nkgt_chkonk:20170808143702p:plain

DDDの話を好む人たちの間では、こういう「本来のドメインモデルと異なる世界」と「ドメインモデルの世界」をつなぐために「腐敗防止層」ってのを作ってそこに変換君を定義することが多いみたいです。ただ、どこまでやるかって話はあって、そこまで複雑じゃなかったり、データソースがAPIと1:1になってるみたいな場合は、ドメイン側に書いちゃってもいいんじゃないの?って判断をすることが私は多いです。ビルダーメソッドをstaticでクラスに生やすみたいな感じですね。

f:id:nkgt_chkonk:20170808143707p:plain

実際のコード例はこんな感じです。

f:id:nkgt_chkonk:20170808143713p:plain

別の問題として、たとえばドメイン側のモデルには「male」って情報を持ってたとして、それを「男性」と表示させます、みたいな表示に関するロジックはプレゼンテーションの関心なんで、ドメイン側にもたせたくないですよね。じゃあこれはViewかViewModelに書こうかなって感じになるんですけど、ViewはまさにDOMとかだし、ViewModelもテストはしにくい部分なので、そこにロジックはやしちゃうとちょっとテストしにくくて嫌ですよね。どうしましょうか。

f:id:nkgt_chkonk:20170808143717p:plain

わたしはHelperっていうクラス作って、それにDomainのオブジェクト食わせて表示用のロジック置くことが多いです。これはVue.js依存の話なんですけど、Vue.jsのdataプロパティってプレーンなjsonしか食ってくれない(クラスのインスタンス食ってくれない)ので、そのためにJSON吐いてくれるくんをここに書いたりしますね。

f:id:nkgt_chkonk:20170808143721p:plain

実際の実装はこんな感じです。これドメインモデルですね。

f:id:nkgt_chkonk:20170808143727p:plain

で、それを表示用のデータにしてくれるくん(Helper)がこんな感じです。

f:id:nkgt_chkonk:20170808143732p:plain

「えー。JSON吐くところちょっと冗長じゃない?」って感じはするんですけど、まあそこはメリットデメリットあって、ある程度複雑になると、「表示に使えるデータなにがあるのかな〜」って見るときにPresentation層のHelperだけみれば全部わかるってのは結構メリットかなと思います。

f:id:nkgt_chkonk:20170808143736p:plain

あとよくある問題として、フォームのバリデーションどこに書くのよって問題があると思います。

f:id:nkgt_chkonk:20170808143742p:plain

わたしはフォームに入力されるものって要するに「アプリケーションに対する入力」だと思うんですよね。で、フォームのバリデーションって要するに「アプリケーションに対する入力として妥当なものなのか?」を検証するものですよね。そこで、わたしはアプリケーションへの入力をモデリングしたCommandってクラスを作って、そこでバリデーションしてます。

f:id:nkgt_chkonk:20170808143748p:plain

実装はこんな感じですね。

発表のあと「フォームオブジェクトでよくね?」って話をしてくれたひとがいました。わたしも実際一時期Formってクラスを作っていたんですけど、それだとどうしても「このフィールドはテキストフィールド」「このフィールドはパスワードフィールド」みたいな情報を入れたくなっちゃって、プレゼンテーションの関心が漏れ出しがちになるって問題があり、結果としてCommandクラスに落ち着いた、という経緯がありました。

「バリデーションってモデルの責務じゃないの?」って話もありました。この「モデル」が何をさすかによりますが、 MVWの話で言えば、アプリケーション層もモデルの一部なので、そこでバリデーションするのは特に問題がないと思います。もしこの「モデル」が「ドメインモデル」のことを指すなら、「そういう流派もあって、そのあたりは議論があります」という感じの答えになると思います。参考になる議論として Always Valid | Greg Young を置いておきます。

f:id:nkgt_chkonk:20170808143755p:plain

また、わたしが迷った点として、ルーティングにまつわる処値をどうやって作るのか、という話がありました。

f:id:nkgt_chkonk:20170808143800p:plain

たとえば、持ってる権限によってデフォルトの表示ページが異なる場合とか、そういう場合にそのURLってどこで組み立てるべきでしょうか。画面遷移って多分プレゼンテーションの責務だと思うんですよね。

f:id:nkgt_chkonk:20170808143804p:plain

でもViewでURLの組み立てロジック書きたくないし、やっぱりViewModelにも書きたくないですよね。というわけで、まあこれも普通にヘルパーにはやしちゃえばいいんじゃないかなーって思ってます。

f:id:nkgt_chkonk:20170808143809p:plain

実装はこんな感じです。ただ、Helper、ほっとくとゴミ箱になるんで、Helperがあまりに肥大化するようなら、「本来ドメインとしてモデルすべきロジックが"表示のためのもの"として扱われたないかな」というのはきにするほうがいいと思います。

f:id:nkgt_chkonk:20170808143814p:plain

その他、様々なところで困ったこととか、「こうやって解決したよ!」って話をみなさんもお持ちだと思いますので、ぜひ懇親会やこのあとのQ&Aで議論させてください!

f:id:nkgt_chkonk:20170808143818p:plain

というわけで、Bパートのまとめに入ります。細々とした「これどこに書くの?」を見てきました。何度も言いますが、正解の設計ってのはないと思っているので、一例として参考にしていただけると幸いです。また、「これどこに書けばいいのかな」ってなったときには、実装パターンや実例に振り回されず、原理原則に立ち返った上で「これこれこういう理由でここに書く」と言えるのが大事かなと思います。

f:id:nkgt_chkonk:20170808143824p:plain

最後に、とても大切なことを言いたいのですが、

f:id:nkgt_chkonk:20170808143828p:plain

同僚でわたしがめちゃめちゃリスペクトしている @STAR_ZERO ってひとがいるんですけど、その人がこんなことを言ってました。「どんな綺麗に見える設計でも、チームメンバーに合意が取れてなかったらそれは間違えた設計だと思うんだよね〜」。それを聞いてわたしは「名言かよ」ってリアルで言ってしまったんですけど、これは本当にそうだと思います。

f:id:nkgt_chkonk:20170808143834p:plain

というわけで、設計に正解はないけれど、問題の質や大きさ、それに外的要因(開発メンバーのスキルとか、ビジネス上雑でもいいから早く作れ!とか)によって、今現在最適な設計というのは導けるはずです。そのためには、原理原則と、それを実現するためのパターンなどが役に立つでしょう。それをチームで議論し、チームで共有しながら進めていくことがとても大切なのではないかと思います。

f:id:nkgt_chkonk:20170808143843p:plain

さて、アニメが終わると最後にエンドカードが出ますね?もちろん息子の写真です。(ここでry

付録として、今回はVue.jsを題材にして考えてきましたが、React,Reduxスタックでアーキテクチャを考える際に参考になるであろう日本語情報を置いておきます。

Almin.js | JavaScriptアーキテクチャ

さいきんReact, Reduxでやっている設計 - non117's diary