1つ前のエントリーで、いわゆる"Model 2"がMVCパターンの用語で説明されるようになったことが、人々のMVCの理解をバラバラにする原因になった、と書いたが、そのことは、前エントリーの参考リンクの先のページでも見られるように、多くのWebページで触れられていることでもあるが、筆者はむしろ、いわゆるGoF本に書かれているMVCの説明が最大の混乱の元だと思っている。"Design Patterns: Elements of Reusable Object-Oriented Software"(邦訳タイトルは「オブジェクト指向における再利用のためのデザインパターン」)、いわゆるGoF本の§1.2にある以下の記述がそれである。
MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input.
MVC also lets you change the way a view responds to user input without changing its visual presentation. You might want to change the way it responds to the keyboard, for example, or have it use a pop-up menu instead of command keys. MVC encapsulates the response mechanism in a Controller object.
A view uses an instance of a Controller subclass to implement a particular response strategy; to implement a different strategy, simply replace the instance with a different kind of controller. It's even possible to change a view's controller at run-time to let the view change the way it responds to user input. For example, a view can be disabled so that it doesn't accept input simply by giving it a controller that ignores input events.
(注:ここでは"disabled"はボタン等が操作できない状態のことを指す)
GoF本の1.2節を開いてまず目を引くのは、図にControllerが無く、1つのModelと複数のViewのみで構成されていることである。上記の引用文にある通り、これは、ControllerがViewの一部として扱われているからである。
MVCの最大の目的はPresentation logicとBusiness logicの分離による安定性の向上(変化する部分と変化しない部分の分離)である、というのはOriginal MVCから一貫した理念であるが、システム全体を(Controller抜きで)ViewとModelに分離すると書くことによって、それを最大限に強調しているのである。これはMVCの本質をわかりやすく表していると思う。
しかし、それから少し読むと違和感を覚えるのが、ユーザー入力を受けたViewがどう振る舞うかを、そのViewのControllerが決める、という記述である。このようなことは、Original MVCには書かれていない。また、Original MVCに書かれている、ControllerからViewへのメッセージ送信についても、GoF本には明確な記述が無い。
もしこれが、Original MVCのControllerの定義にある
Conversely, a view should never know about user input, such as mouse operations and keystrokes. It should always be possible to write a method in a controller that sends messages to views which exactly reproduce any sequence of user commands.と矛盾しないとすれば、ユーザー入力がViewからControllerへ伝搬されるというのは、"views which exactly reproduce any sequence of user commands"の部分を拡大解釈したものであり、Controllerは入力デバイスのドライバーやウィンドウシステムの役割のみを担うものとしていると解釈するのが妥当であろう。
早い話が、JavaのSwingやQt, GTKなどのイベントドリブンなツールキットを用いて実装されるGUI全体をViewだとしているのである。
図にすると次のようになる。
図5-2: GoF本のMVCのコミュニケーション図(動的構造図)
ポイントとしては、Viewがユーザーイベントの配信を制御することが可能になっており、Original MVCではV-C間の関連におけるControllerの役割だったものの大部分がViewに移動している。また、ViewはControllerより先に存在する前提であり、Viewが表示されていない状態からControllerがView上にメニュー画面の表示を要求するようなことは想定されていない(Swing等のGUIフレームワークを使えば、最初からGUIが何か表示されているのが普通だから、想像は難しくない)。
ViewとControllerの関連の向きがOriginal MVCと逆であることをわかりやすくするため、Controllerのデバイスドライバー/ウィンドウシステムとしての役割を省いたのが、次の図である。
図5-3: GoF本の記述に沿ったMVCのコミュニケーション図(簡略化版)
このMVCのモデル(以下、GoF MVCとする)は、GoF本のテーマにはよく乗っており、同書の以下のデザインパターンが使われていると書かれるのは非常に納得できる。
・Observer
・Strategy
・Composite
・Factory Method
・Decorator
Observer patternが使われることは言うまでもないが、Modelが変化すると、observerとして登録されている全てのViewに何か変化があったことが通知されることを実現するのに使われている。
図5-4: GoF MVCにおけるObserverパターン
Strategy patternは、ユーザー入力に対するViewの振る舞いを決めるControllerが、Viewのインスタンス毎に切替可能であることを実現するのに使われている。多くのGUIフレームワークでは、イベントのコールバック関数(リスナー)が動的に登録可能であることに相当する。ここではControllerがViewの一部なのは、上述の通りである。
図5-5: GoF MVCにおけるStrategyパターン
Composite patternは、View同士が包含関係になり、親Viewが受けたイベントを、内包するViewに伝搬することにより、複合ViewをViewと同じように扱うことを可能にするのに用いられている。GUIフレームワークでは、例えばコンテナの中にコンテナを含めることに相当する。Original MVCの"The Model-View-Controller (MVC) Its Past and Present"のP-9にある"Tool as a composite"も、大体同じような内容である。
図5-6: GoF MVCにおけるCompositeパターン
Factory Methodパターンは、各Viewのdefault controllerを得るのに用いられる、とされる。これによって具象Viewから具象Controllerへの依存を無くすことができ、例えば具象ViewがControllerのいずれかのサブクラスを指定してインスタンスをcreateする必要が無くなる。GUIフレームワークでは、起動時のパーツの配置や初期設定をアプリケーションのmainクラスがまとめて保持することが、強いて言えばこれに相当するだろうか。(世の中にはあるのかも知れないが、筆者はあまりControllerのFactoryというのを見たことがない)
図5-7: GoF MVCにおけるFactory Methodパターン
Decoratorパターンは、Viewのサブクラスに共通する機能の追加を、それぞれのサブクラスを新たに作ることなく、委譲を用いて実現するのに用いられる。GUIツールキットでは、例えばGoF本には、各コンポーネント(クラス)にスクロール機能を追加する時に用いられると書かれている。
オブジェクト指向の経験則として、既存クラスの拡張や既存クラスへの機能追加は、派生クラスを作成するより、AdapterやDecoratorなどを使った委譲によって行う方が良いことが多いと言われることがある。小規模な機能拡張で一々、それぞれの機能の有無毎に派生クラスを作成するのは面倒で非効率であることを考えば、納得できる。
図5-8: GoF MVCにおけるDecoratorパターン
世の中にC→Vの関連が無くV→Cの関連があるMVCパターンの図が存在するのは、GoF本の記述が原因であろう。
また、MVCのデザインパターン的な側面を語られる時は大体、V-C間の関連はViewからControllerへの関連であることも興味深い。V-C間の関連がControllerからViewである、Original MVCに則してGoFのデザインパターンが語られることはほとんど無い。
これは、既存の多くのGUIフレームワークに当てはめてMVCを解釈するには悪くないし、MVCが将来そのような考え方に置き換わっていくのは必然なのかも知れないが、それは筆者にはどうしてもMVCの拡大解釈であり、MVCとは別の名前を付けるべきだったように思える。
GoF本に書かれているMVCは、極端に言えば、Controllerが無くても動作可能なMVCアーキテクチャーである。ウィンドウシステムがミドルウェアに、データモデルや表示系アプリがアプリケーション層にあれば、アプリケーション層だけを見るとControllerが存在しない。
実際、GoF本には、何もしないControllerをViewに与えることによって…という記述があるし、Original MVC的にはViewからのModelの更新はあり得る訳だから、システム全体としてController無しでMVCパターンを実装することも可能だということになる。
ControllerはViewとModelの橋渡しに過ぎず、MVCモデルはViewとModelが主役、というのはMVCの本質であり、それを強調することは悪いことではないと思うが、Controllerの役割やC-Vの関連についてほとんど触れられないのは如何なものであろうか。MVCの本質でなくても、Controllerの位置付けはMVCの必須構成要件である。
オブジェクト指向的に考えると、Controllerのオブジェクトは本質的には不要なので、どの方向から考えても結局はControllerの責務は最小にされるべきだという結論になるからだ、としても、ControllerがViewの一部であるように表現してしまうのは、MVCの説明としては問題があるのではないか。
筆者は、GoF本の記述は、テーマ的にMVCについて触れない訳にはいかないから、同書の内容、あるいは現存するGUIフレームワーク(筆頭著者のErich Gamma氏はJavaとの結び付きが強いからAWTだろうか)と整合する為に、このようにされたのだと考えるべきものだと思う。
なお、近年の用語ではMVVMと呼ばれるパターンの方が、Original MVCよりも、GoF本に書かれているMVCに近いと思う。
ところで、今回、Viewクラスのユーザー操作を受けるメソッド名(本文では"onEvent"と記述)を何にするか、結構悩んだ。ネット上でUMLやサンプルコードを結構探し回ったのだが、そもそも抽象Viewクラスにそういう抽象メソッドを定義している例が見つからず、参考になるものが1つも無かったのである。
GoF流のMVCのサンプルコードはJavaで書かれたものが多く、その為、このページのUpListener/DownListenerのように、具象ViewクラスにてViewにとって意味のある"xxxListener"という名前のメソッド(厳密には"xxxListener"はクラス名でメソッド名は"actionPerformed"だが)にしている例がほとんどである。
元々original MVCの定義では、Controllerが生の"user input"を受けて、Viewの用語に置き換えてViewに送信するのであり、そういう役割のControllerはAWTやSwingのようなフレームワークの実装に隠蔽されているので、参考になる例が見つからないのは仕方ないというより、この形のMVCで、Viewの抽象クラスに一般化した"user input"を受ける抽象I/Fを記述しようとするのが間違いなのかも知れない。
しかし、この形のMVCなら、AWTのKeyListenerやMouseListenerのような"user input"の生データを受けるメソッドもViewに表れそうなものなので、Viewにそのような抽象I/Fを記述するのはそれほどおかしくないと思う。
筆者は、このGoF流のMVCパターンは、GUIフレームワークをMVCで解釈する場合においてしか意味が無いと考えているので、GUIフレームワークで"event"とあればユーザー操作起因のイベントが連想されるだろうと考えて、"processEvent"か"onEvent"のどちらかにしようと思った。
Viewのメソッド名に唐突に"xxxListener"とあっては、まずそれが(Java worldの方々を除いて)イベントリスナーのことだとわからず、その前にユーザー操作がイベントとして通知される前提であることもわかりづらい可能性があるので、"xxxListener"は選ばなかった。
"processEvent"でも"onEvent"でもわかり易さはあまり変わらないとは思ったが、"processEvent"だと能動的にイベントを取りに行く感じで、"onEvent"だと受動的にイベントを待つ感じがしたので、よりコールバック的な"onEvent"にすることにした。しかし、"processEvent"や、イベント名が具体的な"onKeyPress"とか"onclick"とかはよく見かけるのに対して、"onEvent"はネットで検索しても意外と使われていないようである。"onSomeEvent"だと語呂は良いが汎化(抽象化)に合わないし、"onAnyEvent"だと全イベントハンドラー共通の処理のように思えてしまう。
Observerパターンだと、一般にObserverに設けられるメソッドの名前は"update"である。"onChange"とかではない。Subjectに"notify"メッセージが飛ぶと、Observerに"update"メッセージが投げられるのだから、変化があったことを知らせるというよりは、より限定的に、更新せよ、それ以外のことはするなと指示している感じである。これに沿うなら、Viewのメソッド名は"processEvent"の方が良いのかも知れない。
ついでに、一応、ObserverとListenerとCallbackの違いを整理する。
- Observer
- 事前に監視対象のオブジェクトに依頼し、そのオブジェクトの状態が変化したことのみの通知を受ける
- Listener
- 事前にシステムに依頼し、特定のイベントが発生すれば、そのイベントの情報を送ってもらう
- Callback
- 事前に定められた場所に処理を登録し、特定の状態に入ったらそれを実行してもらう
Listenerは、登録時に特定の登録先を知る必要が無い所が、ObserverやCallbackと異なる。また、関数やプロシージャーそのものを登録するのでなく、イベント情報のみを受ける所が、Callbackと異なる。イベント受信後に実際にどの関数を呼び出すかは、その時にListenerが決める。また、Listener自体はオブジェクトである所も、Callbackとは異なる。
Callbackは、一般的には登録できる処理が1つのみであったり、最大数("bucket"の容量)が決まっている所が、ListenerやObserverとは異なる。また、利用者が登録先(登録情報を入れる"bucket"の場所)を知っている必要がある所が、Listenerと異なる。
コメント