
UIパターン
追記
この記事の一部を加筆・修正したものを「開発者が知っておくべき、6つのUIアーキテクチャ・パターン」として@ITに転載しています。
- MVVMを追加した上で、アプリケーションモデルとMVVMをプレゼンテーションモデルのバリエーションとして位置づけました。
- MVPの2つのスタイルとして、監視コントローラとパッシブ・ビューを説明しました。
まえがき
Martin Fowlerの"GUI Architectures"を訳したので公開しようと思ったのだが、FAQページに「EAA developmentとかDSLなんかは商業出版するんで例外ってことで」と書いてある。面倒だったので翻訳の公開はやめて、「自分の理解を書く」というスタイルにしようと思う。
Fowler氏が説明しているのは 「フォームとコントロール」、「モデルビューコントローラー (MVC)」、「プレゼンテーションモデル」、「アプリケーションモデル」、「モデルビュープレゼンター (MVP)」の5つ。なお、後ろの3つは、どれもMVCの変種だ。
氏のサンプルでは、「アイスクリーム濃度のアセスメント」というネタになっているが、ここでは「BMIによる肥満度判断」にする。実際のところ、動作はほとんど同じだ。
目次
フォームとコントロール
よくあるGUIのスタイルというか、VBに代表されるポトペタ。「フォーム」は画面そのもののことで、「コントロール」はテキストボックスとかボタンといったウィジェットを指している。フォームにコントロールをぺたぺた貼り付けてレイアウトし、コントロールのイベントハンドラはフォームに置く。
もしデータバインド機構があれば、DBからぶっこ抜いてきたデータとフォームに配置したコントロールとをビジュアルデザイナーで対応付けできて便利。ものによってはDBへの書き戻しなんかもコーディングなしに設定できたりして。ただし、データの単純なCRUDに留まらない場合は、やっぱりコーディングが必要になる。
処理(データの読み書きやコントロールの読み書きなど)をイベントハンドラにシーケンシャルに書き下すことになるので、何をやっているのかがわかりやすくもあるが、読み書きするデータやコントロールが増えてくると同期を取るのが大変面倒だ。UIにおけるTransaction Scriptだな。そんな時は複数のコントロールを複合させたカスタムコントロールみたいなものを作ることになるだろうね。また、イベントハンドラがコントロールにどっぷり依存することになる。その結果、UIなしの自動テストが難しくなる。
モデルビューコントローラー (MVC)
MVCにはあまりにも多くの派生パターンがあるので、「あなたが言っているMVCは私のMVCと違うね」みたいなことが頻発する。いっその事、MVCはNGワードにしたほうがいいんじゃなかろうか、とは俺も思う。なおFowler氏はあくまで「古典的な」MVCの説明にとどめようとしている。
まずモデル。モデルは単なるデータではなく、ドメインオブジェクトであるべきだ。セッターゲッターがあるだけの貧血ドメインモデルじゃない。抽象度の高いメッセージを受け取るというか、呼び出しの粒度が細かすぎないというかね。
次はビュー。ビューは表示(出力)だけを責務とするもの。原則として他者から参照されないし変更もされない。ビューはモデルを監視していて、モデルが更新されたら表示データを取ってきて自分自身を更新する。デザインパターンで言うところのオブザーバー。なので、1つのモデルを複数の異なるビューに対応付けされることもありえる。
そしてコントローラー。俺の理解では、コントローラーの責務は、入力を受け取って適切なメッセージに変換してモデルに送信する、だ。理想的にはモデルに対するプロキシーであるべきで、1つのモデルのセッターゲッターをがしゃがしゃ操作するのはおかしいと思う。モデルの内部状態を管理するのはドメインロジックなんだから、モデルの責務だよねえ。まあ仮にコントローラーがモデルをちょこまか操作するのを許したとしても、ビューを操作してはいけない。これを許しているのはもはやMVCでは無い。なお、ビューと同じく、1つのモデルに複数の異なるコントローラーが対応付けされることもありえる。たとえば、PCの内部時計を設定するインターフェースはCLIでもGUIでもよい、とかそんな感じ。
ってことで、複数のビューがオブザーバーパターンで同時に更新されること(Fowler曰く「オブザーバー同期」)と、コントローラーがビューを絶対触らないこと(Fowler曰く「分離プレゼンテーション」)が古典的MVCの肝となる。
Fowlerの説明では、GUIコントロールはビューとコントローラーがペアになっているらしい。たとえばテキストボックスがそうだ。だから1画面には、画面ビューと画面コントローラーのペアだけがあるわけではなく、複数の小さなペアも存在するのが普通のようだ。だからこその「オブザーバー同期」なんだろう。
ただし、古典的MVCにも問題がある。それは、ドメインに属さない状態やロジックをどこにおけばいいかという問題だ。例を2つ挙げる。1つはサンプルアプリの例で、BMIに応じてテキストボックスの背景に色をつける(やせ=白、標準=黄色、肥満=オレンジ、高度肥満=赤)というロジックがそうだ。「フォームとコントロール」であれば全部イベントハンドラに書けばよかったので問題にならないが、MVCではどこに置くべきか?結局のところ、モデルに置くかビューに置くかのどちらかになる。モデルに置く場合は、ドメインが美しくないのを我慢して、テキストボックスの背景色をモデルに持たせるという方法もあるし、ビューに置く場合は、普通のテキストボックスを継承して、BMIにしたがって背景色が変わるテキストボックスを作るという方法もある。もう1つの例は、ドロップダウンリストの選択状態によって別のコントロールが使用可能になったり使用不可になったりする場合だ。これもモデルに置いていいものか悩ましい。
このような問題を解決するために使われているのが、「プレゼンテーションモデル」、「アプリケーションモデル」、「モデルビュープレゼンター (MVP)」ということらしい。
プレゼンテーションモデル
これは、割とシンプルな解決法だ。ドメインロジックだけを含むモデルをラップし、プレゼンテーションロジックを含むようなモデル(=プレゼンテーションモデル)を用意すると言うもの。
ドメインに関係する属性や振る舞いは通常のモデルに委譲するが、プレゼンテーションに関係する属性や振る舞いは自前で持つようにする。たとえばBMIの例であればテキストボックスの背景色とか、あるいはドロップダウンリストの選択状態とかをプレゼンテーションモデルの属性として持たせれば、ビューはそこを参照するようにすればよい。
古典的MVCの問題はこれで解決できるが、他の点が問題になることもあるかもしれない。1つは、委譲させるためのコードが面倒な場合があるということ。コードをたくさん書かなくても委譲できるならあまり問題にならないだろう。もう1つはテスト対象コードの量。モデル自体はテストしやすいが、実際に動作させるときはコントローラーやビューも必要なわけで、ビューとコントローラーをモデルに繋ぐ部分や、「オブザーバー同期」部分をテストさせることはやはり難しい。ビューとコントローラーの組は1画面に複数あるのが普通(と、古典的MVCのところで説明した)なので、テスト対象コードの割合はその分減る。
アプリケーションモデル
これは、Fowler氏がVisualWorks Smalltalkのやり方を念頭において説明しているパターンだ。残念なことに私はSmalltalkに詳しくないので、私の理解が正しいか確かめる術がない(助けてsumimさん!!)。間違っていたらごめんなさい。
アプリケーションモデルではビューとコントローラーを分けて考えないらしい。元記事ではウィジェットと呼んでいるのでそう書くが、ここは「フォームとコントロール」のようにコントロールと呼んでもいいかもしれない(コントロールとコントローラーが紛らわしいけど)。
基本的なアイディアはプレゼンテーションモデルに近い。プレゼンテーション関連のロジックは具象アプリケーションモデルに置く。プレゼンテーションモデルと違うのはアプリケーションモデルとアスペクトアダプターという2つのクラスが存在していること。この2つのクラスはライブラリに含まれている。では、アプリケーションモデルとアスペクトアダプターは何のためにあるのか?これは、MVC版データバインドとでも呼ぶべき仕組みの実現のためだ。
まずアプリケーションモデルだが、ウィジェットと具象アプリケーションモデルとのバインドが簡単にできる。具体的には、ウィジェットの値が更新されるときにアプリケーションモデルのプロパティも更新され、アプリケーションモデルのプロパティが更新されるときは(オブザーバーパターンで)監視しているウィジェットに通知が行く、という動作をするのだが、その対応付けは具象アプリケーションモデルのプロパティ名だけで指定できる。
次にアスペクトアダプターだが、具象アプリケーションモデルとモデルとのバインドが簡単にできる。具体的には、具象アプリケーションモデルのプロパティをアスペクトアダプターにしておくと、アスペクトアダプターが持つ値の更新はモデルのプロパティに伝播し、モデルのプロパティが更新されるときは(オブザーバーパターンで)監視しているアスペクトアダプターに通知がいく、という動作をするのだが、この対応付けも簡単に指定できる。
というわけで、MVCだと面倒な、オブザーバー同期を構成する部分のコードをあまり書かなくてもよくなる。面倒なところはアプリケーションモデルとアスペクトアダプターがやってくれるからである。かなりうまく作られたフレームワークだと言える。
ただし、このパターンの問題点もやはりデータバインド部分にある。要するに単純なバインドは簡潔に書けるが、複雑な場合はそうではないということだ。例えば、VisualWorksのテキストボックスは、背景色をアプリケーションモデルにバインドすることができない。そうするためにはテキストボックスを継承してカスタマイズしないといけない。また、ドロップダウンリストの現在選択行を保持する場合は、モデルを参照しつつも現在選択行だけは内部に保持するようなアスペクトアダプターを自分で書かないといけない。ということで、複雑な場合はクラスが増えてしまう。それが嫌なら、例えばテキストボックスの背景色についてはアプリケーションモデルから設定するといったように、MVC原則を破るようなコードを許さないといけない。
モデルビュープレゼンター (MVP)
アプリケーションモデルについては、Fowler氏はVisualWorks Smalltalkを念頭に置いている。しかし私はSmalltalkのことはよく分からないので困っていた。するとsumimさんが「VisualWorks Smalltalk の“アプリケーションモデル”で BMI checker」(その1、その2)というエントリーを上げてくださった。さらに、どちらにもumejavaさんがコメントを残している。お二方のおかげで私もVisualWorksについて少し見通しがよくなりました。ありがとうございました!VisualWorksは非商用なら無料でダウンロードできるので、今度遊んでみようかな。
閑話休題。Fowler氏によるアプリケーションモデルの説明でも取り上げられていたが、VisualWorksでは、面倒なプレゼンテーションロジックを実現する場合はアプリケーションモデルがウィジェットを操作することも一般的のようだ。(sumimさんの2個目のエントリーでもそうしていた。)それを押し進めたのが、「モデルビュープレゼンター(MVP)」である。
MVPを端的に表すなら、“ルーズなMVC”あるいは“「MVC」と「フォームとコントロール」の中間”といった所だろう。「モデル」「ビュー」「プレゼンター」の特徴は以下のようになる。
- モデル
- MVCのモデルと同様、純粋なドメインロジックだけを持つ。モデル自身はビューやプレゼンターに依存しない。
- ビュー
- 画面の表示とユーザー入力の受付口を両方担当する。「フォームとコントロール」で言えばコントロールだし、MVCで言えばビューとコントローラー(の一部)が合わさったもの。前回のエントリーでウィジェットと表現していたものと同じ。ただしユーザー入力はプレゼンターに渡す。なお、オブザーバーパターンでモデルを監視してもよい。
- プレゼンター
- MVCのコントローラーと同様、ユーザー入力をモデルに伝える役割を持つ。その上で、オブザーバー同期で実現できないようなプレゼンテーションロジックも持つ。言い換えると、ユーザー入力をモデルに伝えた後、ビューを直接更新してもよい。
(追記)ビューを更新するときは、ビューの実装に直接アクセスするのではなく、インターフェースを経由する。こうすることで、プレゼンタ―がビューの実装に依存しなくなる。よって、プレゼンタ―のテストも容易になる(テスト時はビューを差し替えることができるため)。
さて、今までのUIパターンとの違いが分かるだろうか?違いがより明確になるように、比較表を作ってみよう。
フォームとコントロール | MVC | プレゼンテーションモデル | アプリケーションモデル | MVP | |
---|---|---|---|---|---|
ウィジェットとは | コントロール | ビューとコントローラーのペア | ビューとコントローラーのペア | ビュー | ビュー |
ドメインロジックの場所 | フォーム | モデル | モデル | モデル | モデル |
プレゼンテーションロジックの場所 | フォーム | ビューまたはモデル | プレゼンテーションモデル | アプリケーションモデル | プレゼンター |
オブザーバー同期 | (なし) | ビューがモデルを監視 | ビューがプレゼンテーションモデルを監視 | ビューがアプリケーションモデルを監視 | ビューがモデルを監視 |
フロー同期 | フォームがコントロールを操作 | (なし) | (なし) | アプリケーションモデルがビューを操作 | プレゼンターがビューを操作 |
ということで、いろんなパターンの特徴を紹介した。MVCの変種にはいろいろあり、それぞれ考え方が違うことが分かってもらえたかと思う。
じゃあ実際にどれを選ぶのかはなかなか難しい問題だ。実装のし易さで選ぶというのもひとつの基準だ。その場合は、実行環境(ライブラリやフレームワークも含む)次第で適したパターンが変わるだろう。
他の基準としてはテストのし易さがあげられる。ここで注意しなければならないのは、オブザーバー同期がフロー同期よりテストし易いというわけではないということ。オブザーバー同期はビューなしでドメインロジックを実行することを可能にするので、ドメインロジック部分のテストにはよいが、ビューとモデルの対応付け部分や、ビューが更新される部分のテストには向いていない。
ドメインロジックはさほど複雑ではないが、プレゼンテーションロジックが複雑だという場合であれば、いっそのこと極端なMVPにするという方法もある――オブザーバー同期を無くし、プレゼンターが完全にビューを操作するという方法が。Fowler氏は「慎ましいビュー (Humble View)」と呼んでいる。こうすれば、プレゼンターのテストでプレゼンテーションロジックのほとんどをテストできるということになる。もちろんプレゼンターを動かすにはビューを必要とするが、そこはテスト・ダブル(テスト用の代役オブジェクト)を用意すればいいだろう。