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

やはりおまえらの MVC は間違えている in バックボーンジェーエス

続編の紹介

続編 やはり俺のMVCは間違えている in Backbone.js を書いた。そっちのほうが有益な情報が乗ってると思うけど面白くないかもしれない

以下本編

MVC の話と宗教の話と政治の話と野球の話はしてはいけないそうですがそんなの知るか俺はするぞ

クライアントサイド MVC の話

そもそも MVC の出自が GUI アプリケーションのために生まれてきたものなので「クライアントサイド MVC」などと言う言い方をしなければならない状況がすでに憎いのだけれど、まあそれはおいておく。

「うちは Backbone.js を使っているから MVC でクライアントサイドが作られていて保守性が高いです」みたいなことを言う人間がたまにいるが、Backbone.js をつかったから(あるいは Marionette.js を使ったらから)といって自動的にお前のアプリケーションが MVC になるわけではない。フレームワークの使い方を学ぶ前にそのフレームワークの背景を学べ。

このエントリーで言いたいことはひとつしかない。

V が M の操作メソッドを呼ぶのはMVCではない!!!!!

Backbone.js は M と V のための基底クラス(という言い方は JavaScript においては正確ではないがまあわかって)は提供しているが、C のための基底クラスは提供していない。そのためだろうか、Backbone.js の「入門」エントリとかそういうたぐいのやつでは、View に Model を持たせて(ここまではよい)View のイベントが起こったときに View から直接その Model の状態を変えるメソッドを呼んでいるみたいなことをしていることが多い。

もちろん、Backbone.jsの「使い方」を知るだけならそれで十分ではあるのだけれど、「Backbone.jsを使おう」となるほどそれなりの規模の実際のアプリケーションでそんなことをしていたら破綻が目に見えている。

なのでちゃんと MVC をおさらいしましょう。

  • Model

    • アプリケーションの論理的な部分の操作を担当する。
      • この「論理的」というのはわかりにくいけれど、言い換えれば「UIに関係ない部分」を担当すると思えばよい。
    • Model の状態に変化が起こったときにはイベントを発火する。
    • ある Model は複数のViewに監視されている可能性がある
  • View

    • Modelのインスタンスを持ちそのイベントを購読する。
    • Modelの状態がかわったというイベントを受け取ったら、Modelのインスタンスの「データ取得用」のメソッドを呼びUIを描画する。
    • Modelのインスタンスを保持するが、「Modelのイベントを購読するため」「Modelの値を取得(操作ではない!!!!!!!!!!!)するため」に保持するのである。ViewがModelの値を変更するメソッドを呼ぶことはない!!!!!
    • 普通はユーザーのアクション(なんかがクリックされた、とかそういうの)はこの View が受け取り、そのときはイベントを発火する
    • ほかの View と同じ Model のインスタンスを監視している可能性がある。その場合、その Model の状態が変わりイベントが発行されたときはその View と自分が再描画されることになる。
  • Controller

    • Model のインスタンスを保持するし View のインスタンスも保持する。
    • View のイベントを購読して、そのイベントに応じて Model の操作メソッドを呼んだり View の操作メソッドを呼んだりする(このとき必要ならばViewのデータを参照する)。
    • 複数の種類の Model のインスタンス、View のインスタンス を持ち得る(ていうか多分ほとんどの場合持つ)

いいですか、どこにも「View が Model の操作メソッドを呼ぶ」というパターンが出てこないですね。重要なのは以下の点。

  • ViewはModelの操作メソッドは呼ばない。呼んでいいのはModelのデータ取得用メソッドだけである。
  • Model の操作は View 自身で処理せず、かならず C に一度渡す。

ではなぜそうするべきなのか。

V と M は 1 対 1 ではない

ひとつの理由がこれです。たとえば、Artist モデルと Song モデルがある場合を考えます。このとき、例えば ArtistsView のうち特定の Artist をクリックしたときには、

  1. そのArtistを選択状態にする
  2. 曲一欄にそのArtistの曲一覧を表示する

というような操作が必要みたいなことはよくあると思う。(もしかしたら「購入済みかどうか調べて〜」とかも入るかもしれない)

このとき、V が直接 M を呼ぶような構造になってると、ArtsitView が SongModel も操作する必要があり、あっあれっでもArtistViewはArtiestModelしか持ってなくてSongModelはどこから呼べばいいんだろう、ええいインスタンスもたせちゃえ!とかグローバル(!!!!!!!!)においちゃえみたいになって構造がわちゃわちゃわちゃ〜となってしまうわけです。

しかし C が V のイベントを購読しておいて、かならずイベントは C でハンドリングするようにしてあげれば、C は必要な M のインスタンスの操作メソッドを呼んであげればよい。このとき、クリックされた V がどんな M を保持しているかなんてことは考えなくていい。なぜならば C はいろんな M を持っているし、それが責務だからだ。そして C が M の状態を変えるメソッドを呼んだことで M の状態が変化すれば、その M を監視している V は自動的に画面が更新されるというスンポーである。

これがMVCの基本。これ守って作っててもいろいろ煩雑なアレがあって、それを解決するために MVVM などの MV* なパターンが存在するわけだけれど、それはおく。というかこの基本がわかってればほかのやつも「なぜそれが必要なのか」が理解できるはず。

大事なのは、イベント監視の方向を統一すること、操作の方向を統一すること、データ取得の方向を統一することである。そのため、

  • 監視の方向については
    • C は V のイベントを監視する
    • V は M のイベントを監視する
  • 操作の方向については
    • C が M を操作する (このときMのイベントが発火され、Vが再描画されうる)
    • C が V を操作する (Mを介さないような操作。たとえば表示してたなんかを消すとか)
  • データの取得の方向については
    • V が M のデータを取得する(操作はしない!!!!!!!!)
    • C が V のデータを取得する

というのをを守るとよいです。そのほかの方向の操作や監視が行われていたらそれはヤバいにおいだと言っていいと思う。まあ単純な操作ならばVから直接Mを変化させていいという人もいるが、単純なものであれ M の操作は C に集約させたほうが責務がはっきりしてよいと思う。

大事なことなのでもう一度

  • 監視の方向については
    • C は V のイベントを監視する
    • V は M のイベントを監視する
  • 操作の方向については
    • C が M を操作する (このときMのイベントが発火され、Vが再描画されうる)
    • C が V を操作する (Mを介さないような操作。たとえば表示してたなんかを消すとか)
  • データの取得の方向については
    • V が M のデータを取得する(操作はしない!!!!!!!!)
    • C が V のデータを取得する

もちろんこれは基本であり、これの変奏もたくさん世の中にはあるのだけど、そのときにもこのMVCの基本は頭に入っていて、どう変奏されているのかを意識するべきだと思う。というか、これを理解せずに MV* フレームワーク使っても M と V が密結合した曰く言いがたいものが出来上がるだけです(とくにJavaScript ではそうなりがちな感じがする。Cocoa とかと違って View が Controller にイベント通知する部分がフレームワークで担保されてないからかな?)。Backbone.js が C を提供してないというなら自分で C を作ればよい。それができない人間が Mariotenette.js を使ったところで結局 MVC な構造にはならないのである。

口だけで説明してもアレなので一度 NDS かなんかで「ライブラリを使わない素の JavaScript から学ぶ MVC の基本のキ」くらいやりたい気持ちがある。

追記

Backbone.js の V は V ではないという話がブコメで出てきて、なるほど感すごいある。しかしそうなると今度は Marionnette.js の思想と衝突しますね……。このあたりはどう考えればいいのだろう!る!

やはりお前らのMVCは間違えていなかった in backbone.js

ブコメで理解したけど backbone.js の View はむしろ P で、そうなると M を呼ぶのもむべなるかなという感じである!お前らの MVC は間違えてるかもしれないけどお前らの MVP は間違えてなかった!ごめん!どっちかっていうと backbone.js の View っていう名前が間違えてる!

MV* で大事なのは、見た目と実際のデータ操作を切り離す部分であって、それが「VがMを操作するな」になるのだけれど、BackboneにとってのVはViewではなくて素のDOMエレメントであり、BackboneのViewクラスはDOM要素とデータ操作を切り離す部分を担当していると考えるとView(という名のPresenter)からModelを操作するのも自然ですね。わたしはViewという名前から、その実態もViewであり、単純にDOMをwrap する程度の役割が期待されていると勘違いしていた。

あと、ちょっと話がこじれるところなんだけど backbone.js を wrap した Marionnette.js はViewがViewで C の役割は C にやらせようねっていう思想に見えるがこの場合はbackbone.jsの View はVとして振る舞うべきなんでしょうかね。わたしはそう認識してそうやっていますが。

なんにせよ、MV* な構造でなにかを作る時には、監視の方向とデータ操作の方向、データ参照の方向を常に意識することが必要であるというのは言えると思う。プラットフォームによりその最適なあり方は異なる。ので、プラットフォームごとにいろんなMV* が出てくるのでしょうね。