MVCの動機
MVCという言葉が初めて登場してから30年以上たった今、最早なんだったのか分からないほどMVCの定義は混迷をきたしているわけだが、どれがMVCでMVVMでMVPであるという定義についてあれこれ考察するのは個人的には好きでなくて、「結局何がしたいのか」という動機がぶれていなければ何でも良いと思っている。
じゃあそれは一体何なのかということを自分なりに考えてみたところ、次の一言に落ち着いた。
「ModelはViewに依存したくない」
世間的には(?)ModelとViewを単に「分ける」と説明されることが多いが、私はそれだけでは納得していなくて、依存の方向こそが重要だと思っている。たとえ分かれているように見えてもModelがViewを参照していたら、情報の取得先や表現方法は固定化されてしまう。
ModelはViewの事情から独立して動き、ViewはModelの動きに応じて自由に表現すべき。そういう世界を実現したい。だから依存関係は常に、
View ⇒ Model
となっているのがMVC的には正しい。
さて、ここまでControllerが登場していないのだが、Controllerは実現したいことに対する手段であって、動機とは無関係だ。
では何をするかというと、Modelが変更されたことをもってViewに働けという合図を送るだけ。これが無いとただModelが変更されるだけでViewに反映されないから、というごく当たり前の話なのであるが、そんな退屈な作業はプログラマが自分で書きたくないしできれば自動化されて欲しい。だからControllerはあまり表に出てきて欲しくない。
ここで、Modelの変更をViewに伝えるための手段は大まかに2つに分けられる。
A: Modelへの更新が終わったタイミングで、ControllerがViewに指令を出す
var model = new Model(); var view = new View(model); model.update(e); view.render();
B: ViewがModelの変更を受け取るためのObserverになる
var model = new Model(); var view = new View(model);// 内部でModelに対してリスナーを仕掛ける model.update(e);
Bの方がエレガントだが実装が難しいと思われる。
JavaScriptでMVCする
そんなMVCが最近JavaScriptの世界で流行っている。
従来のWebアプリケーションは、DOM = Modelという認識でやって問題なかったわけだが、最近のアプリは1ページ内で状態をころころ変えるため、それをJavaScriptオブジェクトとして(= Model)独立して管理するようになった結果、DOM = Viewになった。にも関わらず、従来の「DOMを周りからこちょこちょするやり方」の引力が強すぎてなかなか抜け出せない、あるいは大体のアプリがそれで済むためにわざわざ多大なコストを払ってMVCするメリットが見えないというのが現状である。
何はともあれ、HTML5の登場もあってブラウザで何でもやる時代が来たので、JavaScriptでMVCするノウハウを溜めて置くのは良いことだと思う。
前置きが長すぎたので元に戻すと、如何にしてModelがViewから独立して動き、Viewがそれに追従するかという話だった。
ここでは上記2パターンのうち、Aの代表としてAngular.js、Bの代表としてBackbone.jsに登場してもらうことにする*1。
以降、話を簡単にするためにViewとは即ちDOMの事とする。つまり、どのようにしてDOMへの依存が切り離されているかという点についてのみ着目する。逆に言うと他の関心事(例えばパフォーマンス)については全く触れない(同時に沢山のものを比較するとわけわかめになってしまうため)。フレームワークを総合的に判断したい方は別の記事を参照していただきたい。
図の見方
上部がModelで下部がView。水面より下はDOMに依存している。
- M:Model
- U:更新用メソッド(MVCの定義上ここはModelとなっているが分かりやすさのため一応分けておく)
- 矢印:依存の方向。
- 実線:自分で実装する部分。
- 破線:フレームワークに隠蔽されている部分。
- 黒:制御・参照
- 青:input要素からModelへの変換
- 緑:その他のイベント
- 紫:Model変更の通知
- 赤:DOMへの反映
実線の矢印は常に下(View)から上(Model)に向かって伸びているのが好ましい。
矢印はあくまで依存の方向であるため、データフロー・制御フローとは逆になることがあるので注意。
(書いた後で思ったのだが、実際に触れてみないとこの図を見てもまぁピンと来ないと思う。なんというか申し訳ない。)
Angular.jsの場合
水上がAngular.jsのController、水面下がHTMLにあたる。
Angular.jsは専用のModelオブジェクトを用意していないので、図のMは何の変哲もない普通のJavaScriptオブジェクトである。
まず、input要素が更新されると即座にModelに変換される(青)。またはイベントによって更新用のメソッドが呼ばれる(緑)。
これらの指示はHTML(水面下)に記述されるため、水上からセレクタでデータを引っ張る必要がない。また、図に表れない大きな特徴として「オブジェクトをタグに埋め込まずに直接メソッドに渡せる」ことが挙げられる。これによってさらにDOMにアクセスする必要性が低くなっている。
そして更新が終わるとViewが新しいModelを元に更新される(紫 ⇒ 赤)。
ここで、フレームワークが自動で行う部分(破線部分)を全て取っ払うと、実際に書く処理は U ⇒ M だけになっている。ここがアプリケーションにとって本質的な処理であるから理想的と言える。
いくつかのバリエーション
input要素に入力したものが即座にメインモデルになって欲しくない場合は、一時的なModel(m)を宣言することも可能だ。
また、ModelをそのままDOMにするのではなく途中にワンクッション置きたい場合は、クエリ用のメソッド(Q)を置くことも出来る。Qにはclassを書きたくなることがあり、その場合はCSSを意識するため、図では若干水位を上げている。
なお、これらはフレームワークの機能ではなく、単に実装パターンである。
非同期・外部からのモデル更新
他のObserverパターンを使用するフレームワークと違い、Modelがピュアである(専用のModelを用いない)点はメリットのひとつであることは確かだが、逆にControllerの外部から任意のタイミングで変更されたことを検知できないという弱点がある。それは主に、非同期処理による変更と、外部のDOMイベントによる変更である。
その場合どうするかというと、$apply()というメソッドを呼ぶことで、レンダリングを指示することが出来る。
ただし、そのような更新があるたびに$apply()を明示的に呼ぶというロジックを本処理に紛れ込ませたくないため、Angular.jsではController内に隠蔽して暗黙的に実行するための仕組みが用意されている。
例えば、非同期処理の最たる例であるAjaxを行うためには、既に用意されている$htmlという専用のオブジェクトを使うことで、レスポンスによる更新をDOMに反映させることが出来る。ただし、用意されていない場合はそのコードを自分で書く必要がある。この辺りのぎこちなさが個人的には好きでない。
まとめ
Angular.jsはModelとViewの依存関係としては理想的。DOMに一切依存しないコードを書くことが出来る。
ただしController(+HTML)を跨ぐ処理に関してはクリーンさを保つのに工夫が必要。
Backbone.jsの場合
Angular.jsが色々お世話してくれるのに対して、Backbone.jsは必要最小限の機能しか提供しない。
「ObserverパターンでMVCするのに必要かつ一番面倒な部分が実装されている」という印象を個人的には受けた(ただし本家サイトにMVCフレームワークという説明はない)。
かなり自由度が高いので、Backbone.jsを導入したことで何かを諦めるような事態にはまずならないと思っていい。
MはBackbone.Modelクラスのインスタンス。そして、それより下にあるものは全てBackbone.Viewのキャラクターである。
- U:Viewオブジェクトに属する、Model更新用のメソッド達。
- events:jQuery#onを自分の管轄するDOMに一括指定。それぞれに対してセレクタと委譲先(U)を記述する。
- render:DOMのレンダリングを担当。(フレームワークに呼ばれるわけではない)
- initialize:Modelにリスナーを仕掛け、更新を検知したらrenderを呼ぶように設定する。
- template:必須ではないが、見通しの良さから使う人が多いと思われる(Backbone.jsが強く依存しているUnderscore.jsのテンプレートを使うのが一般的)。
input要素を自動でModelにバインドするような機能は持っていないため、他の要素と同じようにイベントを受け取る。
View内のメソッドがModelを更新すると、その変更を検知し、最終的にはrenderメソッドによってDOMに反映される。
いくつかのバリエーション
いくつか…どころの騒ぎではなく、自由度が高いのでバリエーションは豊富にある。
- イベントの情報だけでは足りない場合があり、DOM(jQuery)を参照したくなることがある(青い実線)
- Observerを使わずに、Uからrenderを呼ぶことが出来る(紫の実線)
- そもそもrenderじゃなくてもDOMを更新できる(赤い実線)
これをやり始めると、みるみるうちにロジックがDOMの海に沈んでいってしまう。そうならないためには、ある程度強い意志を持ってDOMヘの依存を切り離す必要がある。
つまりBackbone.jsは、分かっている人でも面倒で失敗しやすい部分を上手く担ってくれるが、分かっていない人でもこれに沿って書けば自然と綺麗なコードに…という類のものではない。
非同期・外部からのモデル更新
ViewがModelのObserverになることは利点が多い。
- イベント発生時、Modelを変更する以外のことを考えなくて良い。
- どのModelをどのViewが更新するかについて、Viewの外部で把握しておく必要がない。
- 特に非同期処理において、Viewが更新されるタイミングを、Viewの外部で把握しておく必要がない。
- 複数のViewがひとつのModelを共有し、それぞれがModelの変更を自分自身に反映することが出来る。
早い話、Modelの変更を受け取れますよというだけなのだが。
図のように外部からの変更があった場合でも難なく自分自身に反映することが出来る(紫の破線がObserver ⇒ renderヘの委譲を示している)。
まとめ
Backbone.jsは自由度が高く、View内においてDOM依存を避けるための強制力を持っていない。
ViewがModelのObserverであることの利点を最大限発揮できる。
結論
DOMとそれ以外の依存関係という観点で見ると、Angular.jsが優れている。個人的にはセレクタの記述から解放されることと、オブジェクトをメソッドに直接渡せる(=属性の埋め込み不要)ことの喜びが大きい。
ただし(この記事のメインの趣旨ではないが)Modelの変更をViewに反映する仕組みとしてはやはりObserverが強く、Backbone.jsを採用する強い動機になっている。
また、今回取り上げたかったが知識不足で断念したものとして、Ext.js、Bacon.js、Polymer.js(Web Components)がある。そのうちこちらも分析してみたい。
というわけで、今回はこれにて!
*1: http://caliper.io/blog/2013/Javascript-Framework-Popularity/ こちらの人気度調査によると、古くから安定して人気のBackbone.jsにAngular.jsが猛追している真っ最中というところ(2013/06時点) 頭文字がAとBで、イメージカラーが赤と青、アーキテクチャも両極端となると、野次馬としてはとても面白い(どうでもいい)。