Backbone.jsを利用したクライアントサイドMVCの導入についてそろそろ書いておくか

jQueryヘビーなアプリケーションの問題点と、MVCによる構造化の必要性

jQueryは、ブラウザ上で動くJSアプリケーションの開発生産性を劇的に向上させました。DOM操作による動的なページ書き換え処理などは、セレクタを使ってちょろっとコードを書くだけで、ほんの数行で記述できてしまいます。
しかし、この方法の延長で、大規模なJSアプリケーションを構築することは果たして現実的でしょうか。例えば「GMail」や「New Twitter」程度の規模のJSアプリケーションを書かなければならないとしたら、どうでしょう?

大規模なJSアプリケーションを開発するには、こういった手法を延長するのではなく、より洗練されたデザインパターンを導入する必要があります。この目的にぴったりのデザインパターンが、「MVCデザインパターンです。
MVCパターンは、Webの世界ではサーバサイドプログラミングで広く知られており、Ruby On RailsDjangoStruts等、MVCの考え方に則ってデザインされたサーバーサイドプログラミングのフレームワークが多数登場しています。一方で、クライアントサイドでのJSアプリケーションにおけるMVCパターンの活用は、まさにこれから旬を迎えようとしています。ここ数カ月間で、クライアントサイドでMVCを実現するためのJSフレームワークがいくつか登場しました。
現在、クライアントMVCのためのJSフレームワークとして、KnockoutやJavaScriptMVC、そしてBackbone.jsが広く知られています。本エントリでは、その中でも特に有望(※主観です)と思われるBackbone.jsを紹介しつつ、クライアントサイドMVCとサーバサイドMVCの本質的な違い、Backbone.jsを用いたアプリケーションの実装方針について考えます。
なお、Backbone.jsが最近アツいとはいえ、まだまだドキュメントやサンプルコードの整備は発展途上であり、フレームワーク自体の自由度も高いことから、Backbone.js使用時における設計の「正解パターン」は未だコンセンサスが無いように見受けられます。したがって、本エントリに記述してある考え方の大半は、あくまで僕個人が「Backbone.jsはこう使えばいいんじゃないかと思った」というレベルのものであり、オフィシャルな裏付けは特段無いばかりか、Backbone.js開発陣の思惑と乖離している可能性も十分にある旨、ご承知おきください。

参考:Knockout vs JavascriptMVC vs Backbone



ここまで前置き(;´∀`)

Backbone.jsのリファレンス等

参考記事

 Backbone.jsの端的なまとめスライド資料。本エントリに似た趣旨の内容がコンパクトにまとめられている。

 エントリ前半の概論を読むと、Backbone.jsのイメージがわきやすい。後半のサンプルは参考になるが、抜けや間違いが多く、そのままでは動かないと思われ。

 Backbone.jsに対する考察が参考になる。

Backbone.jsとは

Backbone.jsは、JSヘビーなクライアントサイドJSアプリケーションを書くために作られたJSライブラリです。Backbone.jsを導入することで、開発者は自分が実現したい処理を、Model、ViewそしてControllerの組み合わせで実装しなければなりません。
Backbone.jsはUnderscore.jsに依存しています。
Backbone.jsはjQueryを置き換えるものではありません。Backbone.jsはjQueryより上位の概念として存在しているライブラリです。Backbone.jsはjQueryには依存していませんが、View内でのDOM操作はjQueryを用いるのが便利であるため、併せて導入したほうがよいでしょう。現在オンラインで見ることが出来るBackbone.jsサンプルコードのほとんどがDOM操作にjQueryを用いているようです。また、MustacheのようなテンプレートエンジンをView内で用いることも推奨されています。テンプレートエンジンを導入することで、View内でのHTML生成コードをスッキリ記述することができます。
以下は、Backbone.jsが提供する主だったクラスです。

(1) Backbone.Model

 いわゆるModelクラス。データの保持や操作を一身に引き受けるクラス。

(2) Backbone.Collection

 Modelクラスの一部機能を切り出したもの。Modelオブジェクトの順序付きリストとして機能する。サーバサイド等にデータを永続化する際のCRUDオペレーションも担当する。

(3) Backbone.View

 いわゆるViewクラス。HTMLのレンダリングを担当する(レンダリング処理を行うメソッドはrender()という名前にすることが多い)。DOMで発生したイベントに対して、簡単にハンドラをバインドできる便利メソッド「events」等が用意されている。View内部でjQueryを併用するとDOM操作がやりやすくなる。また、Mustache等のテンプレートエンジンを用いると、HTML生成コードがスッキリ記述できる。
クライアントサイドJSアプリケーションはイベントドリブンなので、Viewはイベントハンドラの記述等で必然的にヘビーになりがち。Viewの中のrender()メソッドが、狭義での(サーバサイドMVCにおける)Vに相当する。

(4) Backbone.Controller

 いわゆるControllerクラス。URLフラグメント(URLのうち、#以降の部分)に応じたルーティング処理を行うことができる。
Backbone.js下では、Controllerを経由しない、ViewからModelへのアクセスが多発する。これはイベントドリブンな挙動を実現するため、またRESTfulに忠実なURI設計を行うため(後述)。このため、サーバサイドMVCのコントローラに比べると、Backbone.jsのコントローラはスカスカになりがち。

(5) Backbone.History

 Controllerクラスの一部機能を切り出したもの。ルーティング周りを担当する。


サーバサイドMVCとクライアントサイドMVCの違い

デザインパターンとしてのMVCには、分野に応じて様々な定義があるようです。Web系開発者にはおなじみのサーバサイドMVCとは違って、Backbone.jsは、OSネイティブのGUIアプリケーションに近いMVCを提供します。サーバサイドのMVCと、Backbone.jsを用いたクライアントサイドのMVCを、図で比較してみましょう。

左右のMVCパターンを見比べたとき、左と比べて右側が異なっている点はどこでしょうか。

(1) ViewとModelが結びついている

Backbone.jsではDOMへのイベントハンドラをView内のメソッドとして記述します。このためViewがかなりヘビーになります。さらに、View内に記述されたイベントハンドラが直接Modelを叩くため、ViewとModelが密結合になります。
OOPが重要視するクラス再利用の観点からは、ViewとModelが密結合していることは好ましくありません。このため、Backbone.jsでは、Backbone.Eventsというモジュールを内部で導入し、デザインパターンでいうところの「Observerパターン」を提供することで、この問題を軽減しています。
Backbone.Eventsの導入により、Modelは自分を利用するViewについての予備知識を持つ必要がなくなりました。しかし、Viewは自分が利用するModelについて知っている必要があります。これについてはクラス再利用の観点から許容できないと感じる人もいるかもしれません。DOMイベントに対するイベントハンドラを、Controller(もしくはそれに準じた専用クラス)に分離することで、ViewからModelへの直接のアクセスを排除することはできそうではありますが、コード量が増加する割に複雑性は対して低減せず、メリットが薄いものと個人的に考えています。シンプルさを保つためにもViewがModelを直接叩いたほうがよいのではと思います。

(2) Controllerの役割が薄い

Backbone.jsのControllerは、基本的には、与えられたURLフラグメント(http://foo.com/bar#zoo/baz のようなURLで、ハッシュ以降の"zoo/baz"部分のこと)に応じた処理のルーティングのみを担当します。このため、ページ遷移(この場合のページとは、ブラウザの再読込を伴わず、URLフラグメントのみが書き換わるような画面遷移を意味します)が無いような小規模なアプリケーションでは、Controllerがスカスカになるかもしれません。一方で、提供する機能に応じてページを切り替えるアプリケーションでは、ページに応じたルーティングをControllerが担うことになるでしょう。

たとえば、GMailの場合、受信トレイのURLは
 https://mail.google.com/mail/?ui=2&shva=1#inbox
送信トレイは
 https://mail.google.com/mail/?ui=2&shva=1#sent
コンタクトリストは
 https://mail.google.com/mail/?ui=2&shva=1#contacts

といったように、機能(リソース)に応じてハッシュ以降を変更することで、ページ再読込を伴わないAjaxアプリケーションにも関わらず、各リソースに対するRESTfulなURL(※本エントリの最後に注釈を追記しました)を実現しています。これのおかげで、URLによりアプリケーションの状態を復元することができ、Ajaxにも関わらずブラウザの「進む」「戻る」ボタンもごく自然に機能してくれます。Backbone.jsのControllerを活用することで、自作のアプリケーションにこういったURLを極めて簡単に導入することができるのです。

また、一般的にサーバサイドMVCでは、ControllerはViewをキックする役割を担っていることが多いでしょう。Controllerの役割は、Modelから必要なデータを取得し、それをViewに与え、ViewをキックしてHTMLをレンダリングさせます。一連の処理で、常にControllerが中心で仕切っている構図です。このため当然、ControllerはViewの在処を知っている必要があります。
一方、クライアントサイドMVCでは、事情はかなり異なります。クライアントサイドMVCでは、ViewはModelのデータをリアルタイムに反映し続けなければなりません。この点、バッチ処理的に一度HTMLを生成すれば役割が終了したサーバサイドとは違います。このため、ViewはModelが持つデータの変更イベント(Backbone.jsではchangeイベント)を拾い、イベント発生のたびに自らを書き換えるようにします(Observerパターン)。このため、ControllerとViewの間に直接的な関係は生じません。むしろModelとViewが密に連動しているのです。Controllerはあくまで、アプリケーションが提供するリソースの見え方を変化させるキッカケを与えるだけの役割しか担っていないのです。

Backbone.jsがユーザ入力イベントを処理する二つのルート

Backbone.jsでは、ユーザが発生させるイベントを処理するために、二つのルートが想定されています。開発者は、処理の内容に応じてこれら二つを使い分ける必要があります。

(1) Controllerが提供するルーティング機能

URLのハッシュ以降で表されるURLフラグメントを元に、Controllerがアプリケーションの状態を変更する。ページ要素の多くを書き換えるような遷移にはこの機能を使う。(例:GMailにおける受信トレイから送信トレイへの移動等)

(2) Viewが提供するdelegateEvents

Viewが提供する、イベントハンドリングの仕組み。Modelデータの更新や、ページ要素の一部が書き換わるような小機能の実装には、Viewが提供するこの機能を使う。(例:GMailにおける、下書きメールの編集更新、オートセーブ機能、フィルタ設定画面の表示)

Backbone.jsを使ったアプリケーション設計のポイント

(1) ModelはViewを意識してはいけない(ModelがViewを呼び出す際にはObserverパターンを使う)

 前述のとおり。Modelは再利用可能でなくてはならず、特定のViewや特定のControllerに依存するようなコードを、Model内に混ぜてはならない。

(2) ViewはModelを直接叩いてもよい

 前述のとおり。

(3) ControllerはRESTfulに

 URLフラグメントはあくまでURLの一部であるから(※本エントリの最後に注釈を追記しました)、RESTfulに設計しなければならない。フラグメントに「アクション」を記述してはならない。URLが表すものは、あくまでリソースの場所情報であるべきである。
(悪い例)http://foo.com/bar#delete_comment/3 ← URLに「アクション」情報が含まれており、RESTfulでなくなっている。このURLはURLとして意味をなさない。また、このURLを再読込するとヘンなことになってしまうだろう。こういったアクションはdelegateEventの枠組みで、ViewがModelに直接依頼することで処理されるべき。
(良い例)http://foo.com/bar#latest_comments/orderby-dateasc-limit-10

 オンラインで入手可能なBackbone.jsサンプルコードは、いずれも小規模なアプリケーションであることが多い。こういったアプリケーションではページ遷移が発生しないため、ルーティング機能解説のためか、無理やりURLフラグメントにアクションを記述しているものも多く見られる。こういった部分は真似しないほうが良いだろう。

(4) ViewはModelの粒度にあわせて階層化/細分化する

ViewとModelはそれぞれ一対一に対応しているのが望ましい。ViewとModelを対応させることで、あるModelが更新されchangeイベントが生じると、対応するViewが自動的に書き換わるような設計が採用しやすい。また、DOM書き換えの範囲を最小限にすることができるため、描画高速化に寄与する。


Backbone.jsまとめ

(1) クライアントサイドMVCは、サーバサイドMVCと比べ勝手が違う

 Web系エンジニアにとって、MVCは馴染みが深い考え方です。しかし、クライアントサイドにMVCを導入するにあたっては、サーバサイドとの差異が多くあるため、サーバサイドで得た先入観を捨ててかかる必要があるでしょう。

(2) Backbone.jsでは、View→Modelという密結合が生じるが、しかたがない

 前述のとおりです。ViewはControllerを経由せずに、Modelに直接アクセスをすることになります。しかし、Modelの再利用性を高めるため、ModelからViewへのアクセスは、原則としてObserverパターンを用いて実装されることになります。

(3) URLフラグメントはRESTfulに設計する

 アクションをURLに含めてはいけません。URLでアプリケーションの状態を表せることは、アプリケーションにとって非常に大きなメリットになります。URLをRESTful*1に設計することにより、Ajaxアプリケーションにも関わらず、ページのブックマークが可能になり、ブラウザを再起動させても復元したタブに以前と同じ画面が表示され、ブラウザの「進む」「戻る」ボタンもユーザの直感どおりに動作するのです。



大規模なJSアプリケーションを構築する上で、MVCパターンの導入は非常に大きな助けになります。Backbone.jsのようなフレームワークの登場により、クライアントサイドでのMVC導入がこれまでになく簡単に実装出来るようになりました。
現在、この分野で参考になるドキュメントやサンプルコードは、まだまだそれ程多くはないようですが、スマートフォン全盛期の今、クライアントサイドで動くリッチなJSアプリケーションの需要は高まる一方でしょう。今後数カ月で、状況は劇的に改善していくものと思われます。
今は皆が試行錯誤を繰り返している段階であるため、Backbone.jsはユーザの自由度が非常に大きい設計になっているものの、現状のコミュニティの活況を見るにつけ、Backbone.jsを用いたアプリケーション開発の「定番デザイン」が見出されるまで、それほど時間は要さないのではないかと思います。

*1:4/8 5:03追記 はてぶコメントで指摘頂きましたが、#以降のURLフラグメントは厳密な意味でのURL構成要素ではないことを考えると、URLフラグメントでアプリケーション内リソースを表現する手法を「RESTful」と呼ぶのは間違いであることに気づきました。他にどういう言葉を使うのが適切なのか分かりませんが...