読者です 読者をやめる 読者になる 読者になる

実況中継シリーズ Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 - Re.Ra.Ku アドベントカレンダー day 13

前説

丸山です。Re.Ra.Ku. アドベントカレンダー13日目の記事です。前日はiOSアプリのUIをコードで書いてみる話でした。明日はおそらくScalaの話になると思います。

さて、以前も話題にしましたが、builderscon2016が先日開催されました。チケットは3hでSOLD OUT。プラチナチケットと化した参加権ですが、発表する側ならば実質無料で参加し放題!これはいっそ申し訳ないレベルでは!?

というわけで、せっかく発表したのでその内容をなるべく多くの手段で共有したい。そう思い、今回も実況中継シリーズを弊社テックブログで行います。実況中継シリーズというのは、プレゼンをブログで再現するアレです。なお、実際のプレゼンは動画になってYoutubeにアップロードされております。builderscon公式サイトのセッション詳細ページからもご覧いただけますので、よろしければそちらも合わせてご覧ください。なお、先日行われたNDS#50でも再演を行いました。

Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離

導入

f:id:nkgt_chkonk:20161212135336j:plain

本日はこういうタイトルで発表させていただきます。よろしくおねがいします。

f:id:nkgt_chkonk:20161212135338j:plain

簡単に自己紹介をするとこういうものです。文学部卒の文系プログラマなんで、文系プログラマdisには敏感です。Scala好きです。

f:id:nkgt_chkonk:20161212135339j:plain

Scalaスキーとか言っておきながら今日話す内容はタイトルの通りJavaScriptなんですが

f:id:nkgt_chkonk:20161212135340j:plain

Vue.jsの詳しい使い方とか、MVVMとMVPとの違いとか、あるいはモダンなJavaScriptってこう書くんだぜみたいな話はしませんので、そのへんに興味がある場合はちょっと求められているものを提供できないかもしれません。

Aパート Vue.jsで実現するMVVMパターン

f:id:nkgt_chkonk:20161212135341j:plain

今回はAパートとBパートに分かれています。Aパートとしては、MVVMというアーキテクチャパターンでPDS(PDSについては後述します)を実現するといいことがあるよ、という内容について触れ、Bパートでは最近「流行り」のFluxアーキテクチャってのはそんな中でどう位置付けられうるのか、という話をします。ではまずはAパートです。

f:id:nkgt_chkonk:20161212135342j:plain f:id:nkgt_chkonk:20161212135343j:plain

概念的な話から入ってもいいのですが、わたし自身どちらかというと帰納的にものごとを理解するタイプなので、具体的な例題を交えてMVVMとPDSについて見ていきましょう。今回は、画像pickerを例題にします。これがどういうものがというと、はてなフォトライフみたいな場所があって、そこにユーザーがいろんな画像をアップロードしているとします。画像pickerは、そのサーバーに対して「最近投稿された画像をください」とリクエストして、画面に描画します。で、その画像をクリックすると、クリックされた画像は「選択状態」になり、「選択された画像」という部分に表示されます。この「選択された画像」をクリックすると、その画像は「非選択状態」となり、「選択された画像」一覧から消えます。

f:id:nkgt_chkonk:20161212135344j:plain

ちょっと文章で書かれてもわかりにくいと思うので、動画で見ると、こんな感じです(プレゼンではこれ動画になってます)。

画像picker without MVVM パターン

f:id:nkgt_chkonk:20161212135345j:plain

こういうのをこれから作っていくわけですが、PDSがどんな問題を解決するのかを見るためには、まずは「それがないときに何が困るのか」を見ていく必要があります。そのため、まずはjQueryでべたっと書いて行ってみましょう。

f:id:nkgt_chkonk:20161212135346j:plain

さて、jQueryでがんばる場合、HTMLはこんな感じになるんじゃないかなと思います。class="selected-images"っていうからっぽのdivタグと、class="recently_posted_images"っていうからっぽのdivタグがあってこの中身をjQueryでゴリゴリっと書き換えていく感じになりますよね。

f:id:nkgt_chkonk:20161212135347j:plain

で、JavaScriptのほうはこう。あとで細かく読んでいくんでちゃんと読まなくていいです。クラスがいっこあることだけ見ておいてください。

f:id:nkgt_chkonk:20161212135348j:plain

じゃあこのクラスがなにをやっているかっていうと、まずコンストラクタでjQueryのエレメント、っていうんですかね、それを受け取って内部に保持しておいて、

f:id:nkgt_chkonk:20161212135349j:plain

showRecentlyPostedImagesっていうメソッドを叩くと、念のためrecentlyPostedImagesをからっぽにして、ajaxでサーバーから画像一覧持ってきて、それをぐるぐるぐるっと回してimgタグ用意して、ハンドラ設定して、recentlyPostedImagesにぽこぽこぽこっとappendして行っています。

f:id:nkgt_chkonk:20161212135350j:plain

で、さっきimgタグの設定したハンドラが叩かれるとselectImageっていうメソッドがinvokeされます。ここでは、クリックされた画像のsrc要素見て、おなじsrc持ってる画像がすでに選択されてたらなにもしないでreturn。そうじゃないなら新しくimg要素つくって、やっぱりハンドラ設定して、で、selectedImagesにぽこっとappendするみたいなことしてますね。

f:id:nkgt_chkonk:20161212135351j:plain

まあこれでもちゃんと動きます。動くんですけど、すでに結構嫌な予感してますよね。具体的にどういう嫌な予感がするかっていうと、

f:id:nkgt_chkonk:20161212135352j:plain

まず、見ての通りDOMをコードで作ってますよね。

f:id:nkgt_chkonk:20161212135353j:plain

これ、もし「最近選択された画像のところに新しいcss当てたいからclass設定したいな〜」なんてときに、HTML上にはそのDOMが書かれてないので、「えっとこのimgを作ってるコードはここだから」みたいな感じで、ただcss当てたいだけなのにJSの森に足を踏み入れないといけないわけです。うーむという感じしますね。

f:id:nkgt_chkonk:20161212135355j:plain

問題ってそれだけじゃなくて、たとえばハンドラがとっちらかってるっていうのも問題です。選択された画像のクリックハンドラはここで作ってるし

f:id:nkgt_chkonk:20161212135354j:plain

最近投稿された画像のほうのクリックハンドラはここで作ってますよね。散らかってます。

f:id:nkgt_chkonk:20161212135356j:plain

さて、そうすると、たとえば「画像クリックしたらなんか画面がぶっ壊れた!!バグってる!」みたくなったとき、まずは「ところでこのハンドラどこで設定されてるの……」って感じで、またしてもJSの森に足を踏み入れることになるわけです。うーむ。地獄っぽい。

f:id:nkgt_chkonk:20161212135357j:plain

問題はまだまだあって、今って状態がDOMにしかないんですよね、これ、どういうことかというと、「選択された画像はどれか」っていう情報が、DOM上にしかない。そのため、「この画像は選択されてるか」を確かめるためにDOMをさらう必要があるわけです。問題はそれだけじゃなくて、

f:id:nkgt_chkonk:20161212135358j:plain

たとえば「選択された画像一覧をサーバーに投げて、サーバー側でそれを永続化しておいてほしい」みたいな要件が出たときにも困りますよね。選択された画像一覧を得るために、いちいちまたDOMをさらわないといけないわけです。不毛すぎる。

f:id:nkgt_chkonk:20161212135359j:plain

まあそんな感じで、何も考えずにDOMをいじっていくと、こういう感じで破綻が近づいてくるわけです。

f:id:nkgt_chkonk:20161212135400j:plain

ウゥゥゥっていう感じですね。

PDSとは

f:id:nkgt_chkonk:20161212135401j:plain

で、諸悪の根源はなにかってことを考えるんですけど、どうやらこれは「UIを実現するためのコード」と「アプリケーションの挙動」が密結合しているのが問題っぽいぞ、と思い立つわけです。

f:id:nkgt_chkonk:20161212135402j:plain

これ別にわたしが思いついたわけではなくて、そもそもMartin Fowler先生がPresentation Domain Separationっていう考え方を言ってるんですよ(プレゼンテーションとドメインの分離)。

f:id:nkgt_chkonk:20161212135403j:plain

PDSについては以前わたしもやぱちーで発表をしているので、それを参照してもらってもいいかもしれません。

techblog.reraku.co.jp

f:id:nkgt_chkonk:20161212135404j:plain

で、今回の話に必要なところでいうと、要するにPDSっていうのは「UIを実現するプラットフォーム依存のコードと、それ意外の部分を分けよう」っていう考え方です。

f:id:nkgt_chkonk:20161212135405j:plain

ちょっと重要なポイントとして、「ドメイン」っていう言葉があるので、「あーこれってDDDでやったところだ!」なんて思われる方もいるかと思いますが、DDDにおける「ドメイン」という言葉とPDSにおける「ドメイン」という言葉は別のものを指しています。ので、DDDでいうところの「ドメイン」のことはPDSについて語っている間は忘れてください。PDSでいうところの「ドメイン」というのは、「UI(プレゼンテーション)以外すべて」を指します。

画像picker with MVVM

f:id:nkgt_chkonk:20161212135406j:plain

さて、PDSという考え方があることはわかりましたが、じゃあそれはどうやったら実現できるのでしょう。実はMVVMに限らず、MV*はすべてPDSを実現するためのアーキテクチャパターンです。今回は、Vue.jsを利用してMVVMパターンを実現してみましょう。

f:id:nkgt_chkonk:20161212135407j:plain

さて、Vue.jsを利用したアプリケーションの場合、ViewはスライドのようなHTMLテンプレートとして表現されます。v-forとか{{i.url}}とか見慣れないアトリビュートなどが出てきていますが、Vue.jsはこういうアトリビュートを利用してHTMLテンプレートを書くスタイルなわけですね。

さて、jQueryでがんばっていたときと比べて、HTMLはどのように変化したでしょうか。まず、jQueryで頑張っていたときにはHTML上に出現していなかったimgタグが、このテンプレートには出現しています。これはどういうことかというと、DOM構造が余すことなくHTML上で表現できているということです。これなら、imgタグにcssを当てたいから新しいclassを設定したいなんてときにも、テンプレートをいじるだけで済みます。あと、@clickという見慣れないものがあると思いますが、これはVue.jsが提供するイベントハンドラです。こうして、イベントハンドラもHTML上で表現することで、「イベントハンドラがJS上にとっ散らかる」という問題も解決できました。

f:id:nkgt_chkonk:20161212135408j:plain

改めてポイントをまとめておくと、こういう感じです。

f:id:nkgt_chkonk:20161212135409j:plain

なかなか良いですね!

f:id:nkgt_chkonk:20161212135410j:plain

さて、Viewについては見てきたので、今度はモデルを見てみましょう。

モデルってどういうものかというと、その名の通り「アプリケーションをモデリングしたもの」です。モデリングっていうのは雑にいうと「具体的なあれこれから具体性を引っぺがして抽象化すること」ですよね。今回も、具体的なHTMLとかフレームワークとか関係なく、ピュアなJavaScriptでアプリケーションの挙動を書いてみましょう。

今回のアプリケーションならば、「最近投稿された画像」と「選択された画像」というのが動的な要素なので、コンストラクタでそれらをプロパティをして宣言しておきます。

また、今回のアプリケーションの動きとしては、「サーバーから最近投稿された画像を読み込んでくる」「画像を選択する」「画像を非選択にする」という挙動があるので、それらをメソッドとして表現していますね。

f:id:nkgt_chkonk:20161212135411j:plain

さて、これでモデルを記述できました。「DOM」だとか「imgタグ」だとか「css」だとかそういうViewのことばがModelに出てきていないところがポイントです。また、選択された画像についてもModel内に状態を持つことができています。これなら、「じゃあそれをサーバーに送ってよ」と言われたときにらくちんですね。

f:id:nkgt_chkonk:20161212135412j:plain

よい感じです。

f:id:nkgt_chkonk:20161212135413j:plain

さて、これでViewとModelができました。図にするとこんな感じです。プレゼンテーションとドメインが分かれています。

f:id:nkgt_chkonk:20161212135414j:plain

ところで、ViewとModelが分かれているのはいいのですが、今はViewとModelがまったく無関係に存在しているだけで、これではアプリケーションが動きません。そこで、ViewとModelの間でどうにかしてコミュニケーションする必要があります。ここをやってくれるのがMV*の*部分です。当然、MVVMで言えばVMがここをやってくれるわけです。

f:id:nkgt_chkonk:20161212135415j:plain

図にするとこう。よく見る図ですね。ここでポイントは、VMが「Presentation」の側に置かれていることです。VMを「Domain」の側においてしまうと、「UIを実現する以外のコード」がどんどんViewModelに書かれるようになってしまい、ViewModelがどんどん太ります。いわゆる「FatController問題」とか「SmartUI問題」にぶち当たることになるので、あくまでVMはPDSで見た場合「Presentation」側の責務を負っているということを意識しておいてください。

f:id:nkgt_chkonk:20161212135416j:plain

さて、それでは、Vue.jsにおけるVMはどのような責務を負うことでVとMの間を仲立ちしてくれるのでしょうか。Vue.jsのVMは、3つの責務を持っていますが、そのうちのひとつが「Viewのための状態ストア」です。

f:id:nkgt_chkonk:20161212135417j:plain

Vue.jsにおいて、VueというクラスのインスタンスがVMの役割を持ちますが、それのdataというプロパティがViewのための状態ストアとなります。今回ならば、selectedImagesrecentlyPostedImagesを定義していますね。

f:id:nkgt_chkonk:20161212135418j:plain

ここで定義されたものは、Viewからはv-forとか{{someVar}}とかそういうVue.jsが提供するシンタックスを利用して参照することができます。

f:id:nkgt_chkonk:20161212135419j:plain

で、ここがVue.jsのおもしろいところなんですけど、VueModelのdataを書き換えると、databindという仕組みを通じて、ViewであるHTMLが書きかわります。例えばここでは5秒後にVMのrecentlyPostedImagesを書き換えていますが、これを実行すると、

f:id:nkgt_chkonk:20161212135420j:plain

こうなります(プレゼンではここは動画でした)。リロードした瞬間は「最近投稿した画像」が空で、5秒待つと、VMの値が書き換えられて、はい、Viewが再描画されて画像が出てきましたね。

f:id:nkgt_chkonk:20161212135421j:plain

こういう感じで、「databinding」という機構を利用して、ViewModelとViewの間をつなぐことができました。

f:id:nkgt_chkonk:20161212135422j:plain

さて、ViewModelの2つ目の責務は、「Viewからのイベントを待ち受けてModelをdispatchする」です。

f:id:nkgt_chkonk:20161212135423j:plain

Viewに記述された@clickというのがイベントハンドラであることは説明しましたが、たとえばここでselectが呼ばれると、実際にはVMのmethodsに定義したselectが発火します。

f:id:nkgt_chkonk:20161212135424j:plain

じゃあVMのselectメソッドは何をしているかというと、自身が保持するModelのselectメソッドをdispatchしているだけです。

f:id:nkgt_chkonk:20161212135425j:plain

Modelのselectメソッドでは何が起こるかというと、Model自身のselectedImagesを書き換えて、Modelの状態に変化を起こしています。

f:id:nkgt_chkonk:20161212135426j:plain

ここまでを図にするとこうですね。ViewModelがViewのイベントに応じてModelをぶっ叩いて、Modelの状態が変わります。

f:id:nkgt_chkonk:20161212135427j:plain

さて、これだけでは、Modelが書きかわるだけで、UIは書き変わりません。そこで、Vue.jsにおけるViewModelの三つ目の責務を見てみましょう。「Modelの変化を監視し、自身の状態ストアを変更する」です。Modelが変化したときに、その変化をキャッチして自身のデータを書き換えれば、databindingでViewも書き変わりますよね。

f:id:nkgt_chkonk:20161212135428j:plain

で、Modelの変化をキャッチするんだから、まあここはObserverパターンの適用でしょう。

f:id:nkgt_chkonk:20161212135429j:plain

このへんはいろいろ工夫の余地(Rx使う?とかいろいろ考えられるでしょう)がありますが、今回はそこは論点ではないので愚直にObserverパターンを適用しましょう。

通知くんを作って

f:id:nkgt_chkonk:20161212135430j:plain

Modelが通知くんを保持して

f:id:nkgt_chkonk:20161212135431j:plain

Modelのプロパティが書き換わったことを通知するためにsetterでこの通知くんをfireします。

f:id:nkgt_chkonk:20161212135432j:plain

ViewModel側は、あらかじめこのModelの通知くんを監視しておいて、変化があればそれを契機に自身のデータを書き換えます。

f:id:nkgt_chkonk:20161212135433j:plain

図にするとこういう感じですね。

f:id:nkgt_chkonk:20161212135434j:plain

さて、このみっつの責務をもったViewModelの完成系はこんな感じになります。

f:id:nkgt_chkonk:20161212135435j:plain

Viewのための状態ストアがdataにあって

f:id:nkgt_chkonk:20161212135436j:plain

methodsでViewからのイベントをModelをdispatchして

f:id:nkgt_chkonk:20161212135437j:plain

Modelの変化を監視して自身を書き換えるように設定しておくわけです。

f:id:nkgt_chkonk:20161212135438j:plain

これらを図に表すとこうなりますね。

まず、Viewからイベントが起こり、それをVMがキャッチします。

VMはイベントに応じてModelのメソッドをdispatchします。

また、VMはModelを監視しているので、Modelに変化が起こったら自身を書き換えます。

databindingによって、Viewが再描画されます。

矢印は依存の方向を表していますが、この時Domain側がPresentation側に依存していないことに気をつけて下さい。

DomainがPresentationに依存して、UIの言葉をしゃべりはじめると、せっかくMVVMを利用してPresentationとDomainを分けたのに、Domain側にどんどんDOMとかが漏れてきて、だいなしになってしまいます。MVVMパターンを利用して、DomainがPresentationに依存しないようにする、という形で、PDSを実現させているわけですからね。

f:id:nkgt_chkonk:20161212135439j:plain

さて、MVVMによってPresentationとDomainの分離に成功しました。

PDS導入前は、UIとそれ以外がごちゃごちゃに書かれることによっていろいろと問題が出ていましたが、

f:id:nkgt_chkonk:20161212135440j:plain

PDSを導入することで、各レイヤーの責務が明確になり、拡張に対して開かれた感じに設計することができました。

f:id:nkgt_chkonk:20161212135441j:plain

とっても良い感じですね!

f:id:nkgt_chkonk:20161212135442j:plain

ところで、ModelはピュアJSなのでテストしやすいのはわかりましたが、Presentation側のテストってどうやるの?って話についてはしませんでした。ブラウザに依存しちゃうようなPresentation側のテストについては、id:shiba_yu36 さんが良い記事を書いているのでそちらを参照してください。

developer.hatenastaff.com

さて、ここまででAパートは終了です。

Bパート:Fluxアーキテクチャへの接近

f:id:nkgt_chkonk:20161212135445j:plain

さて、AパートではMVVMパターンを利用することでPDSの実現ができる、ということについて見てきました。Bパートでは、Vue.jsによるMVVMパターンがFluxアーキテクチャに接近してゆく様を見ることで、FluxアーキテクチャとMVVMパターンの距離を見ていきたいと思います。

f:id:nkgt_chkonk:20161212135446j:plain

さて、まずは「Fluxアーキテクチャってそもそもなによ」って話から見ていきたいと思います。なんかFacebookが言い始めた概念で、MV*の陥る問題への解決として提言されているようです。

f:id:nkgt_chkonk:20161212135447j:plain

こういう記事があるので、中身を見てみましょう。

f:id:nkgt_chkonk:20161212135448j:plain

すると、「MVCって、MとVの間に双方向データフローがあるせいでえらく複雑になるよね〜」

f:id:nkgt_chkonk:20161212135449j:plain

「Flux使って単方向データフローにするとめっちゃいいぜ!!!」

ってことがかいてあります。

f:id:nkgt_chkonk:20161212135450j:plain

Fluxの基本的なアイデアは、「MとVの間の双方向データフローなくして単方向データフローにしようぜ」ってことだと言えそうです。

f:id:nkgt_chkonk:20161212135451j:plain

じゃあ単方向データフローってなに?って話なんですけど、よく見る図はこういうやつですよね。

f:id:nkgt_chkonk:20161212135452j:plain

ViewからActionが発火して

f:id:nkgt_chkonk:20161212135453j:plain

Dispatcherがどんなロジック呼ぶか決定して

f:id:nkgt_chkonk:20161212135454j:plain

そのロジックがStoreを書き換えて

f:id:nkgt_chkonk:20161212135455j:plain

結果がViewにレンダリングされる。

こういうフローをぐるぐるぐるぐるぐる回すことでアプリケーションを実現しよう、ってアイデアですね。

f:id:nkgt_chkonk:20161212135456j:plain

ところで、われわれのMVVMと比べてみるとどうなるでしょうか。

f:id:nkgt_chkonk:20161212135458j:plain まず、Viewからアクションが発生して

f:id:nkgt_chkonk:20161212135457j:plain

ふむふむふむ。Viewでイベントが発生して

f:id:nkgt_chkonk:20161212135500j:plain

Dispatcherがどんなロジックを呼ぶか決定して

f:id:nkgt_chkonk:20161212135459j:plain

ふむふむふむ。VMがModelのどんなメソッドを呼ぶか決定して

f:id:nkgt_chkonk:20161212135502j:plain

そのロジックでStoreが書き換わって

f:id:nkgt_chkonk:20161212135501j:plain

ふむふむふむ、Modelが書き換わって

f:id:nkgt_chkonk:20161212135503j:plain

その内容でViewが書きかわる

f:id:nkgt_chkonk:20161212135504j:plain

その内容でViewが書きかわる

f:id:nkgt_chkonk:20161212135505j:plain

おなじでは!?!?!?!?!!?となるわけです。

f:id:nkgt_chkonk:20161212135506j:plain

実際、Bパート冒頭で貼った記事にはこういう反応もあるんですね。「ああ、君たちはMVCを再発明したんだね!そしてそれに違う名前をつけたんだ!!」みたいなことが書かれてしまっています。

f:id:nkgt_chkonk:20161212135507j:plain

実際、注意深く設計されたMV*は、データフローは単方向になります。

f:id:nkgt_chkonk:20161212135508j:plain

じゃあFluxって単なる流行なの?意味ないの?というと、

f:id:nkgt_chkonk:20161212135509j:plain

そんなことはなくて、ここから先は独自研究って言われちゃいそうですが、Fluxにもちゃんと価値はあって、そのうちのひとつが単方向データフローの強制です。

f:id:nkgt_chkonk:20161212135510j:plain

MVVMと言えば尾上さんのこの有名な記事がありますが、ここには

f:id:nkgt_chkonk:20161212135511j:plain

それを踏まえて考えれば、ViewModelに公開するModelのインタフェースは以下の二つしかありません。

  • Modelのステートの公開とその変更通知
  • Modelの操作のための戻り値のないメソッド

ということが書かれていますね。

f:id:nkgt_chkonk:20161212135512j:plain

実際、わたしたちのアプリケーションでもVMはModelの戻り値のないメソッドを叩いているだけでした。

f:id:nkgt_chkonk:20161212135513j:plain

逆に言うと、もとの記事にある通り、戻り値のあるメソッドを叩いてその結果を利用してしまえば、VMとMの間に無駄な依存関係ができてきてしまい、ここで単方向のデータフローもぶっ壊れます。

[f:id:nkgt_chkonk:20161212135514j:plain

もう一度言うと、「注意深く設計された」MV*は単方向データフローになるんですね。しかし現実的な問題として、

f:id:nkgt_chkonk:20161212135515j:plain

われわれは注意深くないのです。大切なことなので光らせておきます(プレゼンではこのスライドの文字がポケモンフラッシュしました)。

f:id:nkgt_chkonk:20161212135517j:plain f:id:nkgt_chkonk:20161212135518j:plain

まあそういうわけなので、仕組みで縛って単方向のデータフローを強制するというのはなかなかに価値のあることなのではないかと思います。

f:id:nkgt_chkonk:20161212135519j:plain

もう一点、Fluxアーキテクチャを実現するフレームワークとかはだいたい単一のStoreを持つようなものが多いのですが、この単一のStoreというのもFluxが提供する価値として数えていいとわたしは考えています。

この価値について考えるためには、素朴なVue.jsの難しさについて、もう少し深く見ていく必要があるので、もう少しVue.jsについて深く見ていきましょう。

f:id:nkgt_chkonk:20161212135520j:plain

Vue.jsでは、VMをサブコンポーネントに分けて、Presentation層を分割統治することができます。Presentation層を分割統治するなら、Domain層も分割統治したいですよね。やってみましょう。

f:id:nkgt_chkonk:20161212135521j:plain

親のコンポーネントのViewでは、こんな感じで子コンポーネントを配置します。VMでは、どのコンポーネントがどのVMをロードするか書いてあげます。

f:id:nkgt_chkonk:20161212135522j:plain f:id:nkgt_chkonk:20161212135523j:plain

で、子VMのほうでは、それぞれ選択された画像のみに関心を持つModelや最近投稿された画像のみに関心を持つModelを保持するようにしてみました。

f:id:nkgt_chkonk:20161212135524j:plain

図にするとこういう感じです。

f:id:nkgt_chkonk:20161212135525j:plain

これでうまく動けばいいのですが、ちょっと問題があります。

というのは、今回は「選択された画像」というのは「最近投稿された画像」をクリックすることによって初めて生まれるものなので、selectedImagesというコンポーネントがrecentlyPostedImagesのイベントに依存しているのです。

困ったときには公式を当たってみましょう。

f:id:nkgt_chkonk:20161212135526j:plain

あるコンポーネントで起こったイベントを他のコンポーネントで受け取りたい場合はEventBusを利用するといいよってかいてあります。

f:id:nkgt_chkonk:20161212135527j:plain

しかし、これ、ちょっと嫌な予感がしますね。単純な例ならいいのだけれど、複雑になってくると、このEventBusが渋滞してスパゲッティ化していくことが容易に想像できます。

f:id:nkgt_chkonk:20161212135529j:plain

問題はそれだけではありません。「VMとMの配線問題」と私は呼んでいるのですが、そもそもVMとMはきれいに1:1になるのでしょうか?画面上の右上と左下に、同じ情報を参照する要素があり、片方が編集されたらもう片方も同期して変更されたい、というようなことは、ざらにあります(未読通知とかそういうのを想像してみてください)。この場合、複数のVMが同じModelを参照すればいいでしょう。

f:id:nkgt_chkonk:20161212135530j:plain

しかし、これも複雑になってくると、VMとMの間に複雑な依存関係が生まれてきてしまうことが想定されます。

f:id:nkgt_chkonk:20161212135528j:plain

ウゥゥゥゥっという感じですね。

f:id:nkgt_chkonk:20161212135532j:plain

とは言え、本当はこれは問題ではなくて、今回で言えば、本来凝集性が高いはずのModelをVMに引きづられて分けてしまったりしていて、「そもそもDomain側の設計がタコ」というのが問題だったりするわけです。Model側、つまりDomain側が適切に設計されてさえいれば、こんな複雑なことには本来ならないはずなのです。

f:id:nkgt_chkonk:20161212135533j:plain

とは言え、私たちは注意深くないのです。重要なことなのでまた光らせておきます。

f:id:nkgt_chkonk:20161212135535j:plain

そこで、困った時は公式のドキュメントにあたります。そうすると、「もっと複雑なときはstate-management-patternを見に行ってくれよな」とかいてあって、リンクが貼ってあります。そこを見に行きましょう。

f:id:nkgt_chkonk:20161212135536j:plain

そこを見に行くと、

  • まず単一のStoreを用意します
  • そのStoreに状態をもたせます
  • そのStoreの状態をmutateするメソッドはStoreに生やします

f:id:nkgt_chkonk:20161212135537j:plain

  • そのStoreのstateを複数のVMで共有します

f:id:nkgt_chkonk:20161212135538j:plain

  • 各VMは単一のStoreに対してメッセージを送ることでStoreに生えたメソッドでStoreを更新します

というようなことが書かれています。

これってどっかで見たような話ですね。

f:id:nkgt_chkonk:20161212135539j:plain

実際、Vue.jsの公式サイトでは「こうしてStoreパターンを通じてFluxアーキテクチャにたどり着きました」みたいなことが書かれているわけです。

f:id:nkgt_chkonk:20161212135540j:plain

さて、こうして見てみると、Fluxアーキテクチャというのは、見方を変えると、MVVMパターンに対して、単一の大きなModelを持たせ、単方向のデータフローを強制したもの、とみなすことができるでしょう。

f:id:nkgt_chkonk:20161212135541j:plain

図にするとこうです。

f:id:nkgt_chkonk:20161212135542j:plain

えっ!?!?!?!?!?!?

って感じですよね。嗅覚の鋭いひとはすでに戦々恐々としてると思うんですけど、

f:id:nkgt_chkonk:20161212135543j:plain

「巨大なStoreってまじ!?それが便利って言ってるのって、グローバル変数はどこからでもアクセスできて便利って言ってるのと何が違うの!?」って感じですよね。怖い。

f:id:nkgt_chkonk:20161212135545j:plain

とは言え、実はそんなに怖くないんですこのStore。なぜなら、まず、StoreをmutationできるのはStoreだけです。そのStoreがどのような状態を取りうるのかは、Storeを見ればわかります。また、Storeだけで全てをやる必要はありません。たとえばフォームのバリデーションをしたいなら、そのフォームをモデリングしたFormモデルのようなものをStoreが保持し、そいつに仕事をさせればいいわけです。また、状態の数があまりに多くて見通しが悪ければ、Storeの内部をmoduleという単位に分けて分割統治すればいいのです。実際、Vue.jsのfluxライクな拡張であるVuexのサンプルコードでは、Storeをmoduleという単位に分けています。

f:id:nkgt_chkonk:20161212135547j:plain

図にするとこうですね。Presentation側からは一枚岩に見えるStoreですが、Domain側に注目すればそれは適切に分割統治されています。

f:id:nkgt_chkonk:20161212135546j:plain

見てきたように、単一のStoreは怖くないんですが、ここを適切に分割統治するためには、適切な設計が必要になってくるわけです。なおかつ、Domain側をどのような視点で分割すべきか、という問いには万能の答えはなく、そのアプリケーションがどのような特性を持っているのかを技術的、ビジネス的双方の視点から紐解いて導くしかないものです。まあ銀の弾丸はないので、がんばってやっていきましょう。

f:id:nkgt_chkonk:20161212135548j:plain f:id:nkgt_chkonk:20161212135549j:plain

そんな感じで、Bパートのまとめに入っていきますが、Fluxの価値ポイントとしては、

  • PresentationとDomainの分割構造は一致しない
  • 巨大なStoreという窓口を設けることで、Presentatonからは単一の窓口に見える
  • それによって、Presentationの分割構造に引きづられずにDomainを設計できる

ということが言えるのではないでしょうか。なんというか、責務の分割、という普通のことを言っていますが、

f:id:nkgt_chkonk:20161212135550j:plain

ある種の問題の解決としてFluxアーキテクチャに接近していくことが見て取れたと思います。

f:id:nkgt_chkonk:20161212135551j:plain

ここでBパートは終了なので、まとめとしてCパートに入っていこうと思います。

f:id:nkgt_chkonk:20161212135552j:plain f:id:nkgt_chkonk:20161212135553j:plain f:id:nkgt_chkonk:20161212135554j:plain

  • Veu.jsを利用することで、MVVMパターンを導入し、PDSが実現できることを見てきました。
  • ただ、素朴なVue.jsで綺麗な設計をするのはなかなかに難しい、ということも見てきました
  • そこで、Storeパターンを利用してFluxアーキテクチャに接近することで、問題の解決への補助線を引けることを見てきました。
  • とはいえDomain側の設計はしっかりやらないといけないので、がんばってやっていきましょう。

なお弊社はメンバー募集中です。

Q&A

以上で発表内容は終わりですが、ここから先はbuildersconとNDSで出てきた質問に返答していきます。

  • Q.Viewのコンポーネントの再利用したいって場合ってどうするのがいいの?
    • A.良い質問です!Viewのコンポーネントの再利用は幻想だと思っています。「似たような見た目だけど違う役割」みたいなものを再利用するのはやめたがほうがいいと思います。
  • Q.PDSはわかったけど、localStorageとかってプラットフォーム依存だけどUIのコードじゃないよね、どう考えればいいの?
    • A.良い質問です!PDSという考え方で言えば、Domain側です。しかし、特定のプラットフォームに依存するという意味でいうと、少しP側と性格が似ていて、直接依存するとポータビリティやテスタビリティが悪くなりそうです。ここはちょっと分離したいですよね。そのときに用いるべきなのが「infrastructure層」という考え方です。そのような特定のプラットフォームやミドルウェアに依存するコードはInfrastructureという層に押し込んでしまって、Infrastructureを使う側はInterfaceを通じてそれを利用することで、Infrastructureへの依存を断ち切る(いわゆるDIPですね、そのための方法としてDIなどが使えるでしょう)ということができます。こういうことをやっていくと、「ヘキサゴナルアーキテクチャ」だとか「クリーンアーキテクチャ」だとか呼ばれるものに近づいていきます。