GWT の MVP サンプル
GWT では大きなアプリケーションを作る場合に MVP がオススメなのだそうです.
MVP はいわゆる MVC の変形ですが,POSA 本で唯一翻訳されている Vol.1 にも出ていて決して新しいものではありません.
世の中が (Ajax バリバリになる前の) シンプルな Web アプリで楽をしてる間,こっち方面が劇的に進化していたとかそんなことはありませんでしたか.
じゃあ,本格的にリッチな Web アプリになるとまた 90 年代のような苦しみを味わうのかなぁ.
ともあれ (JW),あの頃の VB や VC++ (MFC) のようにイベントハンドラのスパゲッティにおぼれないために,GWT の中の人達 (?) は MVP をオススメしているようで,解説とサンプルがこの辺にあります.
でもでも,どちらもなんだか微妙です...
どうもですね,ビューとプレゼンターを疎結合にしたいみたいなんですね.
だけど,実際には疎結合になってなくて,余計なことをしちゃってる.
そして一貫性がない.
Contacts
そんなわけで (どんなわけで?),最初のドキュメントで解説されている Contacts
サンプルを見てみます.
このサンプルアプリには 2 つの画面 (プレゼンターとビューのペア) があります.
連絡先の一覧画面と編集画面です.
ここでは一覧画面だけ見ていきます.
まずは連絡先の一覧を表示する画面のプレゼンター実装クラス.
package com.google.gwt.sample.contacts.client.presenter; public class ContactsPresenter implements Presenter { public interface Display { HasClickHandlers getAddButton(); HasClickHandlers getDeleteButton(); HasClickHandlers getList(); void setData(List<String> data); int getClickedRow(ClickEvent event); List<Integer> getSelectedRows(); Widget asWidget(); } private final Display display; ...
このネステッド型として定義されている Display
が,ContactsPresenter
とビューの間のインタフェースになります.
こいつを実装したビューであれば,どんな実装でも ContactsPresenter
とインタラクションできるというわけね.
そして ContactsPresenter
は Display
インタフェースからウィジェットを取ってきてイベントハンドラを登録します.
public void bind() { display.getAddButton().addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { eventBus.fireEvent(new AddContactEvent()); } }); ... }
ふむ...
とても残念ながら,「どんなビューの実装でも」というほどにはこの Display
,ビューの実装を抽象化できていません.
まず,連絡先を追加するにはビューは GWT の HasClickHandler
を実装したウィジェットを使っていなければなりません.
HasClickHandler
を実装しているのはボタンだけでなく,パネルなどマウスでクリックできるものなら多くのウィジェットが HasClickHandler
を実装しているようです.
だからどんなウィジェットを使っていてもプレゼンターは影響を受けない... わけはありません.
例えば Ext GWT (2.x) のウィジェットは現在のところ GWT の標準的なイベントを使っていません.
Ext GWT 3 で対応するとロードマップには出てますけどね.
ともあれ (JW),現時点では Ext GWT を使ったビューは ContactsPresenter
と組み合わせることはできないということです.
その他,コンテキストメニューやショートカットキー,Drag'n'Drop で連絡先を追加できるようにすることもできないでしょう.
できないことはないけど,その場合は ContactPresenter.Display
を修正し, ContactView
を修正し,そして ContactsPresenter#bind()
も修正する必要があるでしょう.
追加ボタンが複数になっただけでも同じです.
つまり,プレゼンターとビューはまるっきり疎結合になっていないのです.
これだったら Display
なんてインタフェースなくてもいいんじゃないでしょうか?
邪魔なだけに見えます.
ウィジェットにイベントハンドラを登録するのもプレゼンターでやるのはおかしく見えます.
Contacts2
続いて Part II.
こちらは XML でウィジェットの構成を定義する UIBinder を使った場合の MVP について.
そのサンプルが Contacts2 です.
実際には前述 (Part I?) の Contacts との差分なので,モジュール名も Contacts のまま.
でもここでは Contacts2 と呼びます.
先の Contacts では,プレゼンター実装クラスはみんな共通の Presenter
インタフェースを実装していただけでしたが,Contacts2 では個別のプレゼンターごとにインタフェースを定義します.
どこに定義するかというと...
package com.google.gwt.sample.contacts.client.view; public interface ContactsView<T> { public interface Presenter<T> { void onAddButtonClicked(); void onDeleteButtonClicked(); void onItemClicked(T clickedItem); void onItemSelected(T selectedItem); } ...
なんということか,今度は個別のビューのインタフェースをトップレベルのインタフェースとして定義して,そのネステッド型として個別のプレゼンターのインタフェースを定義するのです.
そこでプレゼンターの実装クラスはこうなります.
package com.google.gwt.sample.contacts.client.presenter; public class ContactsPresenter implements Presenter, ContactsView.Presenter<ContactDetails> { private final ContactsView<ContactDetails> view; ...
うーみゅ...
こんな感じですよ.
- Contacts の場合
- class XxxPresenter
- interface Display
- class XxxView implements XxxPresenter.Display
- class XxxPresenter
- Contacts2 の場合
- class XxxPresenter implements XxxView.Presenter
- interface XxxView
- interface XxxPresenter
- class XxxViewImpl implements XxxView
UIBinder を使うだけでこんな風に構成を変えざるを得ないってのは,Contacts の MVP のもろさを如実に語ってるんじゃないでしょうか.
前述の Contacts サンプルではプレゼンターの実装クラスがビューからウィジェットを取ってきてイベントハンドラを登録していました.
しかし Contacts2 は UIBinder を使っていて,UIBinder ではビューに定義したイベントハンドラが呼び出されるように裏でごそごそやってくれます.
将来の GWT では UIBinder が MVP に対応して,プレゼンターのイベントハンドラにアノテーションを付ければそれを自動的に登録してくれたりするかもしれませんが,現時点ではそうなっていません.
そこでビューの方でプレゼンターのイベントハンドラを呼び出してあげるようになってます.
@UiHandler("addButton") void onAddButtonClicked(ClickEvent event) { if (presenter != null) { presenter.onAddButtonClicked(); } }
んー...
UIBinder を使うかどうかはビューの実装の選択肢の一つにすぎなくて,それくらいでプレゼンターに影響して欲しくないですよね.
それなのに,個別のプレゼンターごとにインタフェースを用意するかどうか,個別のビューのインタフェースをどこに定義するか,イベントハンドラのルーティングをどこで行うかが変わってしまうというのはいただけません.
つーか UIBinder 使うだけで変わってるところ多すぎ.
実際,Contacts2 には連絡先の一覧画面と編集画面があるのですが,UIBinder を使っているのは一覧画面だけで,編集画面は使っていません.
っていうか,編集画面の方は Contacts サンプルのまま (Contacts2.zip には含まれていないので Contacts.zip のをそのまま使う) なので,両方の流儀が入り交じってます.
プレゼンターとビューを疎結合にしたくてインタフェースを定義しているようなのに,ビューで UIBinder を使うかどうかでこんな風に大きな影響があるというのはまるっきり目的を果たしていないように思います.
Contacts3
そもそもプレゼンターとビューを疎結合にする必要はあるのでしょうか?
一つのプレゼンターに対して異なった複数のビューを使えるようにする...
失うものが何もないなら疎結合を目指してもいいとは思いますが,実際は上で見たようにウィジェットの変更にすら耐えられていないわけで,疎結合は実現していません.
そもそも目指すことに無理があるんじゃないのかな?
モデルとプレゼンター/ビューのペアが疎結合であることには意味があると思うけど,プレゼンターとビューの関係は疎にしてもしょうがないんじゃないかなぁ.
やりたいことは役割を分担してコードをスッキリ整理して見通しをよくすることで,一つのプレゼンターに対して複数のビューを使ったりすることじゃないと思うよ.
そんなわけで (どんなわけで?),プレゼンターとビューの疎結合を諦めて,UIBinder を使おうが Ext GWT を使おうが,一貫した方法でプレゼンターとビューを実装できるようにしてみました.
これを勝手に Contacts3 と呼びます.
疎結合を諦めたということで,プレゼンターとビューのペアは一蓮托生で同じパッケージに置いちゃいます.
んで,画面あるいは画面のグループごとにパッケージを分けた方が整理できていいんじゃないかな.
このサンプルは画面が2つしかないから全部同じパッケージでもいいんだけど,なんとなく一覧画面は list
パッケージ,編集画面は edit
パッケージに分けてみました.
その一覧画面のプレゼンター.
package com.google.gwt.sample.contacts.client.list; public class ContactsPresenter implements Presenter { private final ContactsView view; ...
そしてビュー.
package com.google.gwt.sample.contacts.client.list; public class ContactsView extends Composite { private ContactsPresenter presenter; public void setPresenter(ContactsPresenter presenter) { this.presenter = presenter; } ...
どちらも個別のビューやプレゼンターのインタフェースは用意せず,相方となる実装クラスを直接相互参照してます.
どのビューにも相方のプレゼンターを受け取る setPresenter(XxxPresenter)
メソッドを持たせます.
イベントのルーティングはビュー側でやります.
UIBinder を使った一覧画面の場合.
@UiHandler("addButton") void onAddButtonClicked(ClickEvent event) { if (presenter != null) { presenter.onAddButtonClicked(); } }
UIBinder を使わない編集画面の場合.
public void setPresenter(final EditContactPresenter presenter) { getSaveButton().addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { presenter.onSaveButtonClicked(); } }); ...
そんなわけで (どんなわけで?),中途半端な疎結合を捨てることで,多少は一貫性があり,画面の変更でもインタフェースと実装クラスの両方を同時に修正するようなことがないようにしてみました.
修正したサンプル全体はこちらに置いてます. 消しちゃった.
Contacts と Contacts2 の MVP サンプルで気になったのはそこだけじゃないんだけど (Contacts2 の ColumnDefinition
辺りも微妙な気が),まずはそこだけ見直してみました.
でもまぁ,これだけ小さな画面でこの調子だと,本格的な画面に立ち向かうにはもっと強力な武器が必要だろうなぁという感じ.
GWT 用の MVC/MVP フレームワークもいろいろあるようなので,調べるといいものがあるかなぁ.
っていうか,GWT (Java) に逃げるより,いい加減 JavaScript に立ち向かう方が幸せに近いかもと思ったりしないでもない.
Unit Test
Contacts および Contacts2 が個別のビューごとにインタフェースを用意しているのは,単体テストをしやすくするためという理由もあるかもしれません.
実際,Contacts サンプルには GWT の環境を使って実際のビューをインスタンス化してテストするための ExampleGWTTest
と同時に,GWT 環境を用意せずにビューのモックを使ってテストするための ExampleJRETest
が用意されています.
このサンプルではモックを作成するためのライブラリとして EasyMock を使っているのですが,EasyMock ClassExtension は使われていません.
そのためにビューのインタフェースがないとプレゼンテーションを単体テストできないのでしょう.
しかし,EasyMock ClassExtension を使えば実装クラスに対するモックを作成することだってできるわけです.
っていうか,EasyMock3 では ClassExtension が統合されて単独でもクラスに対するモックが作れるようになったのね.
そんなわけで (どんなわけで),Contacts3 の ExampleJRETest
では EasyMock3 を使って実装クラスであるビューのモックを作成するようにしてます.
注意点としては,GWT の UIObject
というクラスの static
フィールドが初期化される際に GWT の環境がないとかって例外をスローされないようにするためのおまじないが必要です.
protected void setUp() { GWTMockUtilities.disarm(); ... } protected void tearDown() throws Exception { GWTMockUtilities.restore(); }