メモを揉め

お勉強の覚書。

MVCとかMVVMの前の自分まとめ

MVC、MVVMに関する記事を色々読んでいると、
それなりに理解したつもりになっても、いざ具体的な事になるといまいちピンと来ない。

今感じている事、

  • 大規模な開発を経験したことが無いから、必要性が実感できてない
  • サンプルを見てもいまいちメリットが分からない
  • オブジェクト指向も分かったような気になっているだけで、実はあまり分かってないという気がする
  • View→HTML、Model→ロジックとデータ、みたいな単純な対応関係で覚えても捉えどころがない

フレームワークを使っていくうちにコンセプトが理解できるということはあると思うので、とりあえずは手を動かしていきたいのですが、そのとりあえずが分からないのでなんとも気持ちが悪い感じです。 Backbone.jsをドットインストールで触ってみると、スゴイ便利そう。
確かに便利っぽいけどどういう使い方をすればいいのかよく分からない。
むしろ何が分からないのかが分からない。

で、次の記事を読んだ。

GUIアーキテクチャパターンの基礎からMVVMパターンへ

なぜMVCやMVVMなどの考え方が生まれたのか、という所から丁寧に説明されています。

プレゼンテーションとドメインの分離

上のスライドの冒頭で「Presentation Domain Separation (以下PDS)」という考え方が出てきます。

最も有用な設計原則に、 プログラムのプレゼンテーション層(ユーザーインターフェイス)とその他の機能をうまく分ける、というのがあります。

  • プレゼンテーションロジックとドメインロジックが分かれていると、理解しやすい
  • ユーザーインターフェイスはテストがしにくいため、それを分離することにより、テスト可能なロジック部分に集中できる
  • スクリプト用のAPIやサービスとして外部化するためのAPIを楽に追加できる(選択可能なプレゼンテーション部分で見かける)
  • プレゼンテーション部分のコードは、ドメイン部分のコードと違ったスキルと知識が必要
  • 同じ基本プログラムを、重複コードなしに、複数のプレゼンテーションに対応させることができる

Martin Fowler's Bliki 和訳 プレゼンテーションとドメインの分離

 

  • PDSしなかったことにより発生するデメリットは、一般的なOOPがきちんと出来なかった際のデメリットと同様です。規模の大きさに特に比例して影響範囲などの把握が難しくなります。
  • ただ全ユースケースでの処理の概要が一人でも一望できるほど規模が小さいプログラムでは冗長さが目立つかもしれません

このスライドではXAML系プラットフォームでPDSを適用する流れを説明していますが、

プレゼンテーションを「XAMLXAMLの都合が関係ある部分」、ドメインを「XAMLXAMLの都合が関係ない部分」

と定義付けています。

P​D​S​と​は​、​ア​プ​リ​ケ​ー​シ​ョ​ン​を​「​プ​レ​ゼ​ン​テ​ー​シ​ョ​ン​に​関​わ​る​部​分​」​と​「​そ​れ​以​外​」​に​分​け​る​考​え​方​。
プレゼンテーションに関わる部分とは P​r​e​s​e​n​t​a​t​i​o​n​P​l​a​t​f​o​r​m​の​G​U​I​構​築​部​分​(​X​A​M​L​な​ど​の​D​S​L​と​コ​ー​ド​ビ​ハ​イ​ン​ド​な​ど​) P​r​e​s​e​n​t​a​t​i​o​n​P​l​a​t​f​o​r​m​の​(わが)​儘(まま)​に​付​き​合​う​部​分

ということはwebであれば、

「HTML、CSS、DOMとそれらの都合が関係ある部分」と「HTML、CSS、DOMとそれらの都合が関係ない部分」

といった感じでしょうか?
そういったプレゼンテーションプラットフォームの都合による部分と、
それ以外の部分とを、判断する基準があると実際に試してみることができそうです。

MVC系の学習について

さらにMVC系のサンプルでの学習についてこう述べています、

理解が十分でない状態でサンプルからきちんとした概念を読み出そうとしても無駄です。

何​故​な​ら​M​V​C​系​パ​タ​ー​ン​は​「​楽​に​開​発​す​る​た​め​に​」​あ​く​ま​で​も​P​D​S​に​付​随​す​る​メ​リ​ッ​ト​を​手​に​い​れ​る​た​め​の​手​段​な​の​で​、​例​え​ば​M​o​d​e​l​の​中​の​設​計​に​タ​ッ​チ​し​て​い​な​い​わ​け​で​、​サ​ン​プ​ル​コ​ー​ド​に​は​そ​の​他​の​概​念​が​含​ま​れ​す​ぎ​て​い​ま​す​。

中略、

き​ち​ん​と​概​念​か​ら​学​び​ま​し​ょ​う​。​サ​ン​プ​ル​で​学​べ​る​の​は​M​V​C​系​の​考​え​方​で​は​な​く​M​V​C​系​フ​レ​ー​ム​ワ​ー​ク​の​使​い​方​程​度​で​す​。 そ​し​て​そ​う​や​っ​て​学​ん​だ​使​い​方​で​は​、​も​と​も​と​M​V​C​系​の​責​務​分​割​の​目​的​が​わ​か​ら​な​い​も​の​だ​か​ら​そ​の​実​装​要​素​が​何​の​目​的​に​寄​与​し​て​い​る​か​わ​か​ら​な​い​、​結​果​実​装​要​素​の​取​捨​選​択​が​で​き​な​い​

PDSの適用・悩んだ箇所

課題として、以前作ったCSSスプライトのデモPDSの適用を試してみました。

こっちが今回作ったPDS版デモ
ソース->github
見た目、動作自体は以前と同じです。

頭の中にあるのはとにかくPDSを意識するという事だけだったので、手探りでとりあえず思いついた部分からプレゼンテーション層とドメイン層に分けて行きました。
前述の「HTML、CSS、DOMとそれらの都合が関係ある部分」(以下、関係有り)と「HTML、CSS、DOMとそれらの都合が関係ない部分」(以下、関係無し)を基準に考えます。

以下に登場するオブジェクトの名前に付けたViewModelは、
単純に見た目などの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. 指定した数だけ写真のロードを開始する
  2. 1枚ロードが完了するごとに次の写真をロードする
  3. ロードが完了した写真を画面に追加していく

となっています。

すぐに思いついたのは、リストの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つの責務に分割し、preloaderphotosViewはプレゼンテーション層、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」ボタンのクリックイベントに対して、
以下の事を順に実行したい場合です。

  1. プログレスバーのアニメーションを開始する
  2. 写真をクリアする
  3. 入力されたflickrAPIオプションの値をセットする
  4. 入力された最大同時リクエスト数をセットする
  5. 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なども無く、メディエータも本当に一つだけです。
購読の記述は、同じファイルの一箇所にまとめて書きました。
イベントハンドリング1

この図を見て気づきましたが、PDSに気を取られるあまり、
先ほどの分類の「役割」の分割をすっかり無視してハンドリングしてしまいました。

役割の分割を意識してもう一度組み直しました。

イベントハンドリング2

preloaderが追加され、写真の表示・取得のPDSもできています。
購読の記述は役割ごとに分けられ、メディエータも役割ごとに用意しました。
これなら、どこに、どの機能が書かれているか分かりやすいので変更・追加にも強そうです。

命名がまずかったり、責務の分割に不安が残る部分はまだまだありますがとりあえずは、これで良しとします。

まとめ

PDSに関してはだいぶ理解しました、
現時点で分かったことをまとめます。

MVC・MVVMとは
  • ユーザとコンピュータの間において、あるプログラムがPDSを実現するための手段であり、その中の一つの形
  • プラットフォーム固有の都合によって、PDSの為の有効的な手段は違う為、柔軟な考え方が大切
  • 言語やプラットフォームの進化など様々な変化によって、PDSの有効的な手段も変化する為、MVC・MVVMも進化を続けている
  • PDSMVCもMVVMも、あくまでプログラミングを楽にする(生産性を上げる)のが目的、本末転倒にならないように

この課題を通してMVC・MVVMフレームワークの使いどころみたいなものは、少し見えてきた感じがします。

オブザーバパターン・メディエータパターン
  • 疎結合になって、機能の追加・変更・再利用がしやすくなる
  • 疎結合になるほど、手続き的な記述が減るので、一連の流れを一箇所で追いにくくなる

オブザーバパターンになると、プログラムの最初から最後まで追うことができた手続き的で順次的なコード実行から遠ざかります。

JavaScriptパターン」

責務の分割
  • 役割とPDS、両方の観点から分割する
  • まず役割で分けて、それから各役割ごとにPDSを適用すると良さそう
  • 汎用性のある機能を分離する、共通利用、再利用する

ところで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は大事。