MVCとかMVVMの前の自分まとめ
MVC、MVVMに関する記事を色々読んでいると、
それなりに理解したつもりになっても、いざ具体的な事になるといまいちピンと来ない。
今感じている事、
- 大規模な開発を経験したことが無いから、必要性が実感できてない
- サンプルを見てもいまいちメリットが分からない
- オブジェクト指向も分かったような気になっているだけで、実はあまり分かってないという気がする
- View→HTML、Model→ロジックとデータ、みたいな単純な対応関係で覚えても捉えどころがない
フレームワークを使っていくうちにコンセプトが理解できるということはあると思うので、とりあえずは手を動かしていきたいのですが、そのとりあえずが分からないのでなんとも気持ちが悪い感じです。 Backbone.jsをドットインストールで触ってみると、スゴイ便利そう。
確かに便利っぽいけどどういう使い方をすればいいのかよく分からない。
むしろ何が分からないのかが分からない。
で、次の記事を読んだ。
「GUIアーキテクチャパターンの基礎からMVVMパターンへ」
なぜMVCやMVVMなどの考え方が生まれたのか、という所から丁寧に説明されています。
プレゼンテーションとドメインの分離
上のスライドの冒頭で「Presentation Domain Separation (以下PDS)」という考え方が出てきます。
最も有用な設計原則に、 プログラムのプレゼンテーション層(ユーザーインターフェイス)とその他の機能をうまく分ける、というのがあります。
このスライドではXAML系プラットフォームでPDSを適用する流れを説明していますが、
プレゼンテーションを「XAMLとXAMLの都合が関係ある部分」、ドメインを「XAMLとXAMLの都合が関係ない部分」
と定義付けています。
PDSとは、アプリケーションを「プレゼンテーションに関わる部分」と「それ以外」に分ける考え方。
プレゼンテーションに関わる部分とは PresentationPlatformのGUI構築部分(XAMLなどのDSLとコードビハインドなど) PresentationPlatformの我儘に付き合う部分
ということはwebであれば、
「HTML、CSS、DOMとそれらの都合が関係ある部分」と「HTML、CSS、DOMとそれらの都合が関係ない部分」
といった感じでしょうか?
そういったプレゼンテーションプラットフォームの都合による部分と、
それ以外の部分とを、判断する基準があると実際に試してみることができそうです。
MVC系の学習について
さらにMVC系のサンプルでの学習についてこう述べています、
理解が十分でない状態でサンプルからきちんとした概念を読み出そうとしても無駄です。
何故ならMVC系パターンは「楽に開発するために」あくまでもPDSに付随するメリットを手にいれるための手段なので、例えばModelの中の設計にタッチしていないわけで、サンプルコードにはその他の概念が含まれすぎています。
中略、
きちんと概念から学びましょう。サンプルで学べるのはMVC系の考え方ではなくMVC系フレームワークの使い方程度です。 そしてそうやって学んだ使い方では、もともとMVC系の責務分割の目的がわからないものだからその実装要素が何の目的に寄与しているかわからない、結果実装要素の取捨選択ができない。
PDSの適用・悩んだ箇所
課題として、以前作ったCSSスプライトのデモにPDSの適用を試してみました。
こっちが今回作ったPDS版デモ。
ソース->github
見た目、動作自体は以前と同じです。
頭の中にあるのはとにかくPDSを意識するという事だけだったので、手探りでとりあえず思いついた部分からプレゼンテーション層とドメイン層に分けて行きました。
前述の「HTML、CSS、DOMとそれらの都合が関係ある部分」(以下、関係有り)と「HTML、CSS、DOMとそれらの都合が関係ない部分」(以下、関係無し)を基準に考えます。
以下に登場するオブジェクトの名前に付けたView
やModel
は、
単純に見た目などのUIに関するオブジェクトにはView
を、
データを保持したり処理をするロジック部分に関するオブジェクトにはModel
と付けました。
ユーザの入力を受け取る部分
<form>
要素内のDOMとそれらの値を取得するメソッドを持つオブジェクトinputView
です
DOMに直接アクセスし、その中の値にもアクセス出来るので「関係有り」になります。
役割的にもユーザーの目に触れ、直接操作を受け付ける部分なので、文句無くプレゼンテーション層だと思います。
振り返って考えるにこの部分はMVVM系フレームワークで言うところのViewModelになり得る部分で、
このオブジェクトが<form>
を観察し、入力の変化に合わせてその都度自身の値を更新する、
また自身の値の変化に合わせてDOMを更新する仕組みを「双方向データバインディング」というのかと思います。
フレームワークはそのような機能を簡単に実現する手段を提供していて、例えばデザイナーはHTML内にマークアップ的な記述を行うだけで(JavaScriptを一切書扱わなくても)自動的に双方向データバインディングが実現できたりする訳です(多分)。
Flickrとやり取りする部分
FlicrAPIにリクエストを送ったり、返ってきたJSONのレスポンスを保持するオブジェクトflickrApiManager
です。
リクエストする際のオプションを設定したり、JSONから必要な情報を取り出すメソッドを持っています。
ここは直接HTML、CSS、DOMを扱わないし、UIを持たないので「関係無し」になります。
(<script>
タグでJSONPを使っているので、じつはDOMを扱っているという事に気がついたのはずっとあとの話)
そのため、初めはドメイン層に分類していましたが、
後にプレゼンテーション層に変更しました。
なぜドメイン層ではないのかというと、FlickrAPIの都合に関係のある部分という意味で、
ドメイン層と切り離すべき、FlickrAPIに対するプレゼンテーション層であると考えた為です。
UI=プレゼンテーションという固定観念がありましたが、
PDSは人とコンピュータの間だけでなく、コンピュータとコンピュータの間にも適用できます。
人間ではなく、コンピュータ相手の Web Services だって、プレゼンテーション部分です。
ですから、ドメイン部分のコードと Web Services 部分のコードをごちゃまぜにしてはいけないのです。外部APIにしてもそうです。
「Martin Fowler's Bliki 和訳 プレゼンテーションとドメインの分離」
写真を表示する部分
ここは非常に悩みました。
「関係有り」と「関係無し」だけでは対処できないケースにぶつかったからです。
この部分でやりたいことは、
- 指定した数だけ写真のロードを開始する
- 1枚ロードが完了するごとに次の写真をロードする
- ロードが完了した写真を画面に追加していく
となっています。
すぐに思いついたのは、リストの1と2を「関係無し」とし、3を「関係有り」とする分け方だったのですが、これは思ったようには行きませんでした。
画像のロードをどう制御するか
HTMLで<img>
要素を使う場合、ロードが始まるのは<img>
がページに追加された時です
つまり、写真のロードを開始するということ自体がDOMの都合に関係してしまっているのです。
ロード完了時の判定についても、<img>
要素のonload
にコールバック関数を設定するのが常套手段だと思いますが、
これもDOMの都合に関係してしまうのです。
そういう事情があるので、リストの1、2、3をまとめて一つのオブジェクトで処理しようとも思ったのですが(逃げ)、
今回の目的はPDSを適用することにあるので、ちゃんと分離させてみます。
画像を先読みする
まず、ロード開始のタイミングを管理する手段としてpreloader
というオブジェクトを用意しました。
これは<object>
要素のdata
属性にスクリプトやCSS、画像などのURLを設定し、
不可視な状態でページに配置する方法(JavaScriptパターンに載ってた方法)で、先読みを行うオブジェクトです。
3つの責務に分割
preloader
を作ったことで、
ページに写真を追加したり削除したりするオブジェクトphotosView
から、
写真のロードに関する機能を分離することが出来ます。
さらに、写真のURLリストを保持し、同時にロードする枚数などを管理するphotosModel
から、
写真のロードに関する具体的な実装を分離することも出来ます。
最終的には3つの責務に分割し、preloader
、photosView
はプレゼンテーション層、photosModel
はドメイン層に分類しました。
こうして考えてみると、preloader
はHTMLやDOMに「関係有り」ですが、
ユーザとの間を取り持つView
では無かったり、
色々一概には言えない事があるので、柔軟に考えて行く必要があると思いました。
プログレスバーを表示する部分
プログレスバーのアニメーションや見た目に関するメソッドをprogressbarView
に、
他のオブジェクトからのデータ入力や進捗度の計算をprogressbarModel
に分離しました。
フレーム更新用メソッドの呼び出しはrenderer
に分離
progressbarView
は、アニメーションのフレームを更新するメソッドを持っていますが、
一定間隔で呼び出す機能はrenderer
というオブジェクトを作り分離させました。
アニメーションの数が増えた場合に、
各オブジェクトがsetInterval
等でループを回すと、
オブジェクトの数だけループが並走することになり、どんどん重くなってしまいます。
renderer
は、アニメーションしたいオブジェクトの代わりに、フレームを更新する為のメソッドをまとめて実行します。
他のオブジェクトはrenderer
にメソッドを登録するだけでアニメーションを行うことが出来ます。
最終的な分類
スライドを見て「基準がはっきりしてれば仕分けできそう」という印象とは裏腹に、
この作業はとても苦労しました。
未だに自信が持てない部分もあり、これはもうたくさん経験して慣れていくしか無いと感じます。
役割\PDS | プレゼンテーション(ユーザ) | プレゼンテーション(API等) | ドメイン |
---|---|---|---|
ユーザの入力 | inputView | - | - |
Flickrとのやり取り | - | flickrApiManager | - |
写真の表示・管理 | photosView | preloader | photosModel |
プログレスバーの表示・管理 | progressbarView | - | progressbarModel |
アニメーションの更新 | renderer | - | - |
オブザーバパターン
オライリーの「JavaScriptパターン」という本の中でオブザーバパターンに触れられていたのですが、
ちゃんと使ったことはありませんでした。(良い本です、オススメです)
オブザーバパターンとは、
あるオブジェクトが別なオブジェクトのメソッドを直接呼び出すかわりに、
あるオブジェクトは別なオブジェクトの発行するイベントを購読し、
通知を受け取ったオブジェクトが自身のメソッドを実行するようにすることで、
オブジェクト同士の疎結合を促す為の仕組みです。
これを使って、PDSで分類したオブジェクト同士をつなげて行きます。
プロパティのコピーによる継承
まずは「JavaScriptパターン」で使われている、
makePublisher
関数を使ってオブジェクトにイベント通知の機能をコピーします。
このmakePublisher
は、イベントを通知したり購読者を登録したりする「発行者としての機能」をまとめたpublisher
オブジェクトのプロパティを、
他のオブジェクトにコピーする関数です。
この関数を使うことであらゆるオブジェクトを発行者にすることが出来ます。
(既存のプロパティと名前が被った場合は容赦なく上書きします )
var publisher = {
_subscribers: {
any: [] //イベントの型:購読者の配列
},
on: function (type, fn, context) {
type = type || "any";
fn = typeof fn === "function" ? fn : context[fn];
if (typeof this._subscribers[type] === "undefined") {
this._subscribers[type] = [];
}
this._subscribers[type].push({ fn: fn, context: context || this});
},
remove: function (type, fn, context) {
this.visitSubscribers("unsubscribe", type, fn, context);
},
fire: function (type, publication) {
this.visitSubscribers("publish", type, publication);
},
visitSubscribers: function (action, type, arg) {
var pubtype = type || "any",
subscribers = this._subscribers[pubtype],
max = subscribers ? subscribers.length : 0,
i;
for (i = 0; i < max; i += 1) {
if (action === "publish") {
subscribers[i].fn.call(subscribers[i].context, arg);
} else {
if (subscribers[i].fn === arg && subscribers[i].context === context) {
subscribers.splice(i, 1);
}
}
}
}
};
function makePublisher (o) {
var i;
for (i in publisher) {
if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
o[i] = publisher[i];
}
}
o._subscribers = { any: [] };
}
実装を見てみると、イベント通知・購読の仕組みとは、
「購読する」→発行者の持つ購読者リストへのメソッドの追加。
「イベントを通知する」→自身の持つ購読者リストのメソッドを順に実行。
であることが分かります。
購読者リストにはメソッドとそのコンテキスト(this値となるオブジェクト)をセットで登録するようになっていて、
publisher.on("eventtype", "methodName", context);
// publisherがeventtypeイベントを発行する時、contextのmethodNameメソッドを呼び出す
という風に書くことで購読できます。
購読者には特別な機能は必要ありません。また、makePublisher
を使うことで購読者もまた発行者になれます。
発行者はfire
メソッドを使ってイベント通知を行います。
その際、呼び出されるメソッドの引数となるデータをメッセージとして送れます。
publisher.fire("eventtype", message);
// messageは購読者リストのメソッドに引数として渡される。
// context.methodName(message);
オブジェクトの実装
次の事を意識しながら作業を進めました、
- オブジェクト同士は互いの詳細を知らない
- 自身のプロパティと引数で受け取ったデータにのみアクセス出来る
- 一つのオブジェクトが色んな仕事を引き受けない、ほどよく分割された小さい仕事を受け持つ
- インターフェイスはなるだけ再利用できるような汎用性を持たせる
- オブジェクト同士はイベントの発行と購読でやり取りする
(イメージ)
- 1つの役割に特化
- モジュールの中にきちんと機能が収まってる感じ
- 責務が漏れ出してない感じ
メディエータパターン
少し実装が進んでくると、オブザーバパターンだけだとキツイという場面が出てきました。
例えば、
複数のメソッドを決まった順序で呼び出したい時
具体的に言うと「Search Flickr」ボタンのクリックイベントに対して、
以下の事を順に実行したい場合です。
- プログレスバーのアニメーションを開始する
- 写真をクリアする
- 入力されたflickrAPIオプションの値をセットする
- 入力された最大同時リクエスト数をセットする
- flickrAPIにリクエストを送信する
単に各オブジェクトがクリックイベントを購読するだけでは、
呼び出される順序は保証されません。
実際には、書いてある順に登録・実行されますが、
購読の一部を一旦解除し再び登録するだけで、簡単に順序が変わってしまいます。
匿名関数で処理をまとめる
これは匿名関数で包めば解決できます。
inputView.on("searchclick", function () { //「Search Flickr」がクリックされた時
progressbarModel.run(); // 1
photosModel.clear(); // 2
flickrApiManager.setAPIOptions(inputView.getOptions()); // 3
photosModel.setProperties({ maxConcurrentRequest: inputView.getMaxConcurrentRequest() }); //4
flickrApiManager.sendRequestJSONP(); //5
}, null); // 順番にメソッドを実行する
しかし今度は、順番は保証されますが匿名関数への参照がないので、
購読の解除はできなくなってしまいます。
これも、この匿名関数をプロパティとして持つオブジェクトを用意すれば解決できそうです。
他のオブジェクトのメソッドを直接呼び出せるというのは、やはり強いです。
メディエータとは
「メディエータ」は「仲介者」という意味で、あるオブジェクトと他のオブジェクトとの間に入って仲を取り持ちます。
- メディエータは、関係のある、全てのオブジェクトについて知っている
- 各オブジェクトはメディエータを通して他のオブジェクトと情報をやり取りする
- そのアプリケーションに強く依存するのでメディエータ自身は汎用性が低くなる
メディエータパターンの目的は、各オブジェクトが直接やり取りする相手を仲介役1つだけにすることで、
やり取りの経路を減らし、構造をすっきりさせることにあリます。
なので、今回は純粋なメディエータパターンとは違いますが、
他のオブジェクトの抽象度を上げて、
アプリケーションへの依存度を、メディエータに集中させるという意味では同じです。
メディエータは他のオブジェクトのメソッドを直接呼び出しますが、
他のオブジェクトからメディエータのメソッドの呼び出しは、
オブザーバパターンを使います。
情報の不一致が起こる時
もう一つの問題もメディエータで解決できます。
写真を管理するphotosModel
と、
プログレスバーの状態を管理するprogressbarModel
の間でのやり取りです。
配列を送るイベントと数値を受け取るメソッド
photosModel
は配列を渡そうとしますが、progressbarModel
は数値を受け取ろうとします。
photosModel
はURLの配列から<img>
要素の配列を生成し終えた時に、
"photosready"
イベントを発行します。
progressbarModel
はそれを購読し、
<img>
要素の数を、自身の持つ「進捗の割合を計算する為の分母」に、
setDenominator
メソッドを使って代入しようとします。
この時photosready
イベントはメッセージとして、
「<img>
要素の配列」を購読者リストのメソッドに渡しますが、
setDenominator
メソッドが欲しいのは「配列」そのものではなく、
「配列の長さ」なので、メッセージを送る側と受け取る側で型の不一致が起きてしまいます。
setDenominator
メソッドが引数の種類によって処理を変えたり、メッセージの型を合わせることも出来ますが、
モジュールの汎用性を維持しようとすれば、
"photosready"
イベントは、準備出来た写真の配列を渡すのが自然ですし、
setDenominator
は数値を受け取るのが自然です。
このような形で、2つのオブジェクト間のやり取りだけでは上手くいかない場面も出てくると思います。
メディエータが間に入ることで2つのモジュールは型の違いを意識すること無くやり取りできます。
var mediator = {
setDenomiPhotosLength: function (photos) { // メッセージとしてphotosを受け取って
progressbarModel.setDenominator(photos.length); // photosの長さを取り出すだけ
},
};
photosModel.on("photosready", "setDenomiPhotosLength", mediator); // photosreadyをmediatorが購読する
オブジェクト同士の連携
購読=イベントハンドリング
「購読」とは世に言う「イベントハンドリング」の事です。
下の表の用語は大体同じ様な意味で使われていると思います。
- イベントの購読
- イベントの監視
- イベントハンドリング
- イベントリスナ・ハンドラの設定・登録
- イベントの発行
- イベントの発火
- イベント通知
- 通知
- 呼び出される関数
- イベントリスナ
- リスナ
- イベントハンドラ
- ハンドラ
- コールバック
- コールバック関数
- メッセージ
- コールバックに渡される引数
- イベントオブジェクト
細かい用語の違いに悩まされて一向に頭に入ってこなかった記憶があります。
文脈によって呼び方が違うだけで、基本的には同じ意味です。
(使い分けないといけない場面もあるのかもしれませんが)
購読の記述をどこに書くか?
一番最初のイベントハンドリングの状態を図にしたものが下図です。
むしろ、分かりにくくなったような気がするのが不思議ですが、
せっかく作ったので載せておきます。
まだpreloader
なども無く、メディエータも本当に一つだけです。
購読の記述は、同じファイルの一箇所にまとめて書きました。
この図を見て気づきましたが、PDSに気を取られるあまり、
先ほどの分類の「役割」の分割をすっかり無視してハンドリングしてしまいました。
役割の分割を意識してもう一度組み直しました。
preloader
が追加され、写真の表示・取得のPDSもできています。
購読の記述は役割ごとに分けられ、メディエータも役割ごとに用意しました。
これなら、どこに、どの機能が書かれているか分かりやすいので変更・追加にも強そうです。
命名がまずかったり、責務の分割に不安が残る部分はまだまだありますがとりあえずは、これで良しとします。
まとめ
PDSに関してはだいぶ理解しました、
現時点で分かったことをまとめます。
MVC・MVVMとは
- ユーザとコンピュータの間において、あるプログラムがPDSを実現するための手段であり、その中の一つの形
- プラットフォーム固有の都合によって、PDSの為の有効的な手段は違う為、柔軟な考え方が大切
- 言語やプラットフォームの進化など様々な変化によって、PDSの有効的な手段も変化する為、MVC・MVVMも進化を続けている
- PDSもMVCもMVVMも、あくまでプログラミングを楽にする(生産性を上げる)のが目的、本末転倒にならないように
この課題を通してMVC・MVVMフレームワークの使いどころみたいなものは、少し見えてきた感じがします。
オブザーバパターン・メディエータパターン
オブザーバパターンになると、プログラムの最初から最後まで追うことができた手続き的で順次的なコード実行から遠ざかります。
「JavaScriptパターン」
責務の分割
ところでViewModelって何?
ここまで色々書いてきましたが、ViewModel(以下VM)に対する認識はまだあやふやなままです。
DOMはHTML要素に一対一で対応していて、文書の構造を表現しているオブジェクト。
それに対して、Viewを抽象化して、Modelから扱いやすい形にしたものがVMなのでしょうか?
Viewの抽象化ってなんでしょうか?
VMが入ることによってViewの差し替えが容易になるのは実感していますが、
うまく言葉にして説明できない部分がのこります。
あとスライドの終盤で出てくる、
Presentation Modelパターンとの違いについて。
MVVMは、
元来マイクロソフト社のユーザーインターフェースサブシステムである WPF(Windows Presentation Foundation)やSilverlightの世界で生まれた考え方
Model View ViewModel - Wikipedia
XAMLの特徴に深く根ざした仕組みである事。
XAMLに関しては知識が足りないので、分からないのですが、
XAMLを使用するWPFなどのテクノロジ以外で使用されるMVVMは実質Presentation Modelと変わらず、Viewの抽象化などは出来ない。
Model View ViewModel - Wikipedia
らしいです。
まだ分からない部分がたくさん残っていますが、
とりあえずPDSは大事。