12/4の記事(AngularJSを使ったWebアプリのアーキテクチャ設計)で書くと言ったまま放置していたので、AngularJSのMVCパターンについて書いてみたいと思います。
AngularJSのMVCについては、12/19のお前のAngular.jsはもうMVCではない。と言われないためのTutorialというすばらしい記事がありますが、本記事ではもう少し抽象的な内容を扱ってみようかと思います。
MVW(Model-View-Whatever)パターンとは
MVCパターンには、MVC2、MVP、MVVMなど数多くの派生パターンがあります。
目的は同じなのに派生パターンがたくさんあるのは、それぞれのプラットフォーム固有の問題(フレームワークの違いや、サーバサイドかクライアントサイドかの違いなど)によってMVのの役割が異なるからです。
AngularJSは公式ページで"Superheroic JavaScript MVW Framework"と名乗っているので、またMVWなんていう新しいパターンが出てくるのかとゲンナリするかもしれません。
でもこれ、MVWという新しいパターンが提唱されているわけではないようです。
AngularJSの開発者はこんなことを言っていました。(引用元)
Having said, I'd rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework - Model-View-Whatever. Where Whatever stands for "whatever works for you".
「MVについて議論するのは時間の無駄だから、そんな暇があったらコードを書け。MVの*の部分なんて"Whatever"でいいんだ。」という主張のようです。
言いたいことは分かりますが、だからといってWhateverなんて名前をつけたらControllerに何でもかんでも詰め込んでFat Controllerになってしまいそうです。
というわけで、時間の無駄だと言われてしまうのかもしれませんが、AngularJSのMVWパターンについて解説したいと思います。
Model・View・Whateverの役割分担
MV*パターン適用の目的はプレゼンテーションとドメインの分離です。
そこで、プレゼンテーション層とドメイン層に大きく分けた上で、それぞれの関係を図示してみました。
以降で各役割について解説します。
Model
Modelは他のMV*パターンと違いはありません。つまり、プレゼンテーションに関わらない部分すべてがModelになります。
アプリケーションにもよりますが、次のような役割を持つことが多いでしょう。
- ビジネスロジック
- データの入れ物
- サーバーサイドとの通信
- ローカルストレージ
- etc.
他のフレームワークとの違いがあるとすれば、基本的にイベント通知の処理を実装する必要がない(例外はありますが)ところでしょうか。PureなJavaScriptのオブジェクトが使えるので実装が楽になりますね。
また、AngularJSのDI機能を利用してControllerとModelの関係を疎結合にすることができます。可能なところはどんどんService化するのがよさそうです。
View
Viewの役割は、レイアウトとプレゼンテーションロジックに分けることができます。
レイアウトを独自のDSLで記述したり、コードで記述するようなフレームワークもありますが、AngularJSはHTMLでレイアウトを記述します。
ただしAngularJSでは、Directiveという機能で新しいタグや要素を自由に追加することができるので、HTMLの形をしたDSLと言ってしまってもよいかもしれません。
DOMを操作するような複雑なプレゼンテーションロジックは、Directiveの中に閉じ込めてしまうので、HTMLのレイアウトは宣言的に記述することができ、可読性も高くなります。
Controller & Scope
Scopeの役割
Scopeは画面を描画するために必要な状態をストアする役割です。
MVVMパターンを知っている人にとっては、ViewModelと同じような役割を担っていると言えば分かりやすいでしょう。
クライアントMVCフレームワークを使わなかった場合、画面への描画内容と、描画するための状態(プレゼンテーションステート)は区別せず、DOMに両方の役割を持たせるのが一般的だと思います。
しかし、その方法では状態を管理することが難しく、ソースコードの可読性も低くなってしまいます。
そこで、AngularJSでは、DOMは画面の描画のためだけに使い、状態の保持はScopeで行うように役割分担をしています。
これにより、データの操作方法は明確になり、同一のデータを複数の場所に描画したり、異なる方法で描画するといったことが行い易くなっています。
Controllerの役割
ControllerはScopeをセットアップするための役割で、次のような仕事をします。
- Scopeにサービスの呼び出しを結びつける
- Scopeの保持するデータを初期化する
- Modelのイベント通知をScopeに結びつける
Controllerにはビジネスロジックもプレゼンテーションロジックも書けるし、何かと太りがちな存在です。
ビジネスロジックはModelへ、プレゼンテーションロジックはDirectiveやFilterへ移譲し、できるだけControllerが太らないように気をつけなければなりません。
Controllerの分割
画面に要素を追加していくと、Scopeのメンバが増え、Controllerに渡すServiceの数も増えていきます。
もしそれが多すぎるのだとしたら、1つのControllerに役割を持たせすぎている可能性があります。
1画面につき1 Controllerにする必要はないので、画面内の要素ごとに分割したり、階層構造を作ったり、ng-repeat内の1要素ずつにControllerを割り当てたり、必要に応じて分割しましょう。
例えば、次のようにサイドバーと入力フォームと一覧表示部分と全体の親をそれぞれ別のControllerに分割したりします。
<div ng-controller="MainController">
<div ng-controller="SideBarController">
サイドバー
</div>
<div ng-controller="InputAreaController">
入力フォーム
</div>
<div ng-controller="ListControlelr">
<div ng-repeat="item in items">
<my-directive data="item" ></my-directive>
</div>
</div>
</div>
コントローラを分離するとコントローラ間でデータ共有をする必要がでてきますが、手段はいくつか用意されています。
Controllerを再利用しないのであれば親子関係を作ってやりとりし、そうでなければイベントでやりとりするのがおすすめです。
共通のサービスを使ったり$rootScope
を使ったりするのはグローバル変数と変わりないので、最終手段にするのがよいかと思います。
参考情報
本記事の内容は下記のスライドの考え方を参考にさせてもらっています。
XAML系プラットフォームの知識がなくても理解できる内容ですので、ぜひともあわせて読んでみてください。
おしまい
AngularJS Startup Advent Calendar 2013みごと完走ですね。
参加された皆様、特に発起人で13本もの記事を書かれた@matsuzanさん、お疲れ様でした!
読んでくれた皆様もありがとうございました!
始まる前は登録者も少なくて完走できるかどうか心配だったのですが杞憂でしたね。
この調子でAngularJSが盛り上がっていって欲しいものです。