GWTでMVPアーキテクチャ その3 GINと愉快な仲間たち
明けましておめでとうございます。相変わらず読みにくい文章ですがよろしくお願い致します。
今年もあまり記事を書かなそうですが、頑張りたいと思います。
さてここ最近はひたすらappengine + gwt しかやっていないわけですが、
年末辺りにやったGWT のMVPアーキテクチャについて書きたいと思います。
毎回のとおり、翻訳能力が低いため、間違っている部分があるかと思いますが、見守っていただけたら幸いです。
ということで前回GWTのActivity + Place について記載しましたが、今回はそれにGWT用のDIコンテナGIN *1 を含めてより簡単にActivity + Placeを実現したいと思います。
GINとは
GINはGWT用に作成されたDIコンテナ*2でGoogle製のDIコンテナ Guiceをもとに作成されています。
Guice自体は設定ファイルでなくJavaのコードで設定を書くDIコンテナとして、appengineでも利用できるなど*3
個人的にはかなり好きなDIコンテナです。
利用方法もほぼほぼGuiceと同じですが、AOPが非サポートなどGWT特有の癖があったりします。
細かな使い方などなどは公式サイトやググッてください。
テンション的に各元気があったらいつか多分書きます。。。
ってことで実装!!
今回は上記のGINを利用して、Activity + Placeを楽にしてみます。 ちなみに参考にしたのはGWTのMLです(ここ)。
なお今回の記事はGWT2.1のMVPが分かっている前提で書くのであしからず。
わからない方は前回の記事を読むことをおすすめします。
まずはDIの設定 Moduleクラス
まずはDIの設定を行います。Guiceでは前述のとおり、Javaで設定を書きます(Moduleクラスといいます)。GINでも同様にAbstractGinModuleを継承した、クラスに設定を記載してきます。
すべての設定を1クラスに記述してしまうと管理がめんどくさいので
Guice(GIN)ではこのModuleクラスを分けることができます。
なのでMVPの設定モジュールを作成します。
package stk.client.inject; ※import public class MvpModule extends AbstractGinModule { @Override protected void configure() { } @Provides @Singleton public EventBus eventBus() { return new SimpleEventBus(); } @Provides @Singleton @Named("contents") public SimplePanel contentsPanel() { SimplePanel simplePanel = new SimplePanel(); simplePanel.addStyleName(Resources.INSTANCE.site().contentsPanel()); return simplePanel; } @Provides @Singleton public PlaceController placeController(EventBus eventBus) { return new PlaceController(eventBus); } @Provides @Singleton public ActivityMapper activityMapper() { return new AppActivityMapper(); } @Provides @Singleton public ActivityManager activityManager( ActivityMapper mapper,EventBus eventBus,@Named("contents") SimplePanel contentsPanel) { ActivityManager activityManager = new ActivityManager(mapper, eventBus); activityManager.setDisplay(contentsPanel); return activityManager; } @Provides @Singleton public PlaceHistoryHandler placeHistoryHandler( AppPlaceHistoryMapper mapper,PlaceController placeController,EventBus eventBus,TopPlace defaultPlace,MyInjector injector) { mapper.setFactory(injector); PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(mapper); historyHandler.register(placeController,eventBus,defaultPlace); return historyHandler; } }
今回のMvpModuleクラスでは@Providesアノテーションを利用した方法で設定を行っています。
この方法はあるDIの設定を行いたいクラスに初期化処理を入れたい場合などに非常に便利です。
それでは各メソッドについてみていきます。
configureメソッド
configureメソッドはDIの設定を各メソッドです。通常はここにどのインターフェースにどのクラスをDIさせるかなどを記載していきます。
また後記しますが、他のModule読み込みもconfigureメソッド内で記載します。
たとえばこのクラス内のeventBusメソッドをconfigureメソッド内で同じ設定で記載すると以下のようになります。
@Override protected void configure() { bind(EventBus.class).to(SimpleEventBus.class).in(Singleton.class); }
このようにModuleクラスではいろいろな設定の書き方があります。
ちなみに今回わざわざeventBusメソッドに切り出しているのはなんとなく(というより横並び感)なので気にしないでください。
またGuiceを利用したことがある方向けですが、GINではbind().toInstance()がないので、toInstanceを利用するようなケースも@Providedで記載する必要があります。
eventBusメソッド
eventBusメソッドは、Activity + Placeで利用するEventBusインターフェースの設定を行っています。
@Singletonアノテーションを記述することで、EventBusをSingletonとして設定できます。
また@ProvidesメソッドをModule内で記述することにより他のクラスでDIできるようになります。
contentsPanelメソッド
contentsPanelメソッドではViewを挿入するためのSimplePanelの設定を行っています。
@Namedアノテーションを指定することにより"名前付"を行い、後記のactivityManagerのように
DIを行う際、同様のアノテーションを利用することで、
この名前の付いたSimplePanel(つまりここで作成されているインスタンス)を利用出来るようになります。
placeControllerメソッド、activityMapperメソッド
eventBusと同じです。placeControllerではEventBusをModule内でメソッドインジェクションすることにより、
eventBusメソッドで作成したEventBusを利用しています。
activityManagerメソッド
activityManagerメソッドでは、引数に@Named("contents")を設定することで、contentsPanelメソッドで作成したSimplePanelをDIさせ、acitivityMangerに設定しています。
これによりPlaceが変更されActivityが呼び出される際にこのcontentsPanelが利用されるようになります。
placeHistoryHandlerメソッド
placeHistoryHandlerでは、ついでにAppPlaceHistoryMapperの設定を行っています。
AppPlaceHistoryMapperに関しては後ほど細かく書きますが、PlaceのFactoryとしてInjectorを指定しています。
またデフォルト(初期画面表示など)用のTopPlaceなどの設定もここで行っています。
MvpModuleの読み込み MyModule
次に上記で作成した、MvpModuleを読み込みする親のModuleを作成します。
package stk.client.inject; ~ import public class MyModule extends AbstractGinModule { @Override protected void configure() { install(new MvpModule()); //bind displays bind(TopDisplay.class).to(TopView.class).in(Singleton.class); } @Provides @Singleton public LoginInfo loginInfo(){ return new LoginInfo(); } } <|| ><p>configureメソッドにてinstallメソッドを利用し、MvpModuleを読み込んでいます。<br> またMyModuleではログイン情報を保持するためのLoginInfoの設定も行っています。<br> ログイン情報を@Singletonで宣言していると不思議に感じるかもしれませんが、<br> GWTでいうSingletonは各クライアントごとのSingletonの為問題ありません。</p>< ** InjectorとModuleの紐付け ><p>InjectorはGINで設定した各インスタンスを取得するためのインターフェースです。<br> 私の感覚ですが、Seasarで言うところのS2SingletonContainerみたいなものですが、<br> 通常のDIコンテナと異なり、Injectorからインスタンスを取得するためには<br> Injectorにメソッドを定義していく必要があります。<br> GinではこのInjectorを作成するには、GinInjectorを継承したインターフェースを作成する必要があります。</p>< >|java| package stk.client.inject; ~ import @GinModules(MyModule.class) public interface MyInjector extends Ginjector { LoginInfo getLoginInfo(); PlaceController getPlaceController(); ActivityManager getActivityManager(); EventBus getEventBus(); GwtAuthServiceAsync getAuthService(); TopPlace getTopPlace(); TopPlace.Tokenizer getTopTokenizer(); PlaceHistoryHandler getHistoryHandler(); MainView getMainView(); @Named("contents") SimplePanel getContentsPanel(); TopDisplay getTopDisplay(); }
@GinModulesアノテーションを利用して、上記のMyModuleとInjectorを紐付けています。
Injector内では、各利用するクラスのGetterを記述し、あとあと利用出来るようにしています。
これでGIN側の設定は完了です。こんどはMVPフレームワークをより簡単にするために、Placeクラスなどを作成します。
ActivityとPlaceを紐付けするActivityPlaceの作成
前回書いたMVPフレームワークでは、AppActivityMapperにてinstanceofを利用することにより、
PlaceとActivityの関連付けを行っていました。
今回はPlaceとActivityを事前にひもづけることによりinstanceofをほとんど利用せず、
AppActivityMapperを記述できるようにします。
そのためにはPlaceクラスを拡張した抽象クラス、ActivityPlaceクラスを作成します。
package stk.client.framework; ~ import public abstract class ActivityPlace<T extends Activity> extends Place { @Inject Provider<T> provider; protected String token = ""; public void init(String token){ this.token = token; } public String getToken(){ return this.token; } public T getActivity(){ return provider.get(); } abstract public static class Tokenizer<E extends ActivityPlace<?>> implements PlaceTokenizer<E>{ @Inject Provider<E> provider; @Override public E getPlace(String token) { E place = provider.get(); place.init(token); return place; } @Override public String getToken(E place) { return place.getToken(); } } }
まずActivityPlaceクラスでは、Genericsを利用しPlaceとActivityを紐付けています。
providerフィールドに@Inject指定し、Providerクラスをフィールドインジェクションしています。
Providerクラスはクラスを生成するためのクラスで、provider.get()メソッドを呼ぶことにより、
Genericsにより指定されたクラスを生成することができます。
今回はActivityを継承したT(TはActivityPlaceを継承したクラスで任意に設定します。)クラスを生成することができます。
getActivity()メソッドではこのproviderを利用し、Tを生成・返却することで、PlaceからActivityの生成を可能にしています。
ActivityPlace内で宣言された、TokenizerではこのActivityPlaceの生成を行います。
前述のMyInjector内にて呼び出しメソッド(例えばgetTopPlaceTokenizer)を作成し、
AppPlaceHistoryMapperにてMyInjector内のTokenizer作成メソッドを呼ぶことで、
Tokenizerが作成、さらにはTokenizerからこのActivityPlaceが作成されます。
例えば下記のようなActivityPlaceを継承した、クラスを作成します。
package stk.client.place; ~ import public class TopPlace extends ActivityPlace<TopActivity> { @Override public String getToken() { return ""; } @Override public void init(String token) { } @Prefix("!top") public static class Tokenizer extends ActivityPlace.Tokenizer<TopPlace> implements PlaceTokenizer<TopPlace> {} }
PlaceからActivityの作成を行う
今度はPlaceからActivityの作成を行うActivityMapperの作成をします。
package stk.client.framework; ~ import public class AppActivityMapper implements ActivityMapper { @Inject public AppActivityMapper() { super(); } @Override public Activity getActivity(Place place) { if (!(place instanceof ActivityPlace<?>)) { return null; } ActivityPlace<?> activityPlace = (ActivityPlace<?>)place; return activityPlace.getActivity(); } }
どおでしょうか、前回のAppActivityMapperにくらべinstanceofの記述がほぼなくなり、
非常にすっきりしたのではないでしょうか?
前回のAppActivityMapperではPlaceが増えるたびにAppActivityMapperを書き換える必要がありましたが、
GINを利用したことにより、その依存が消えAppActivityMapperを変更する必要がな くなりました。
次にHistory Token と Placeの紐付けを行う、AppPlaceHistoryMapper
前回のAppPlaceHistoryMapperでは@WithTokenizersを利用することにより、History TokenとTokenizerの紐付けを指定していました。
今回はTokenizerがGINにより作成される必要があるため、WithTokenizersアノテーションを利用できません。
そのため、前回AppPlaceHistoryMapperを作成するために継承した、PlaceHistoryMapperを利用するのではなく、
TokenizerのFactoryを指定できる、PlaceHistoryMapperWithFactoryを継承します。
package stk.client.framework; ~ import public interface AppPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<MyInjector> { }
PlaceHistoryMapperWithFacotryのgenericsとして、MyInjectorを指定しています。
これによりGWT 2.1のMVPフレーワークがFactoryとして指定されたMyInjectorより、
PlaceTokenizerを継承したクラスを返却するメソッドを探し出し、Tokenizerを生成するようになります。
余談ですが、このgenericsにしてするFacotryは特に指定されたクラスを継承する必要がありません。
PlaceHistoryMapperWithFactoryにはこのFactoryのGetterもないため、ものすごく不思議な感じがします。。。
細かな仕組みはここでは書きませんが、このへんがGWTの離れ業なんでしょうね。
あとは適当にActivityとPlaceを作成
あとは実際に利用する、ActivityとPlaceを作成します。
package stk.client.presenter; ~ import public class TopActivity extends AbstractActivity { private final MyInjector injector; @Inject public TopActivity(MyInjector injector){ this.injector = injector; } @Override public void start(AcceptsOneWidget panel, EventBus eventBus) { TopDisplay topDisplay = injector.getTopDisplay(); panel.setWidget(topDisplay); } }
package stk.client.place; ~ import public class TopPlace extends ActivityPlace<TopActivity> { @Override public String getToken() { return ""; } @Override public void init(String token) { } @Prefix("!top") public static class Tokenizer extends ActivityPlace.Tokenizer<TopPlace> implements PlaceTokenizer<TopPlace> {} }
で最後に呼び出し
さいごにこれらのクラスの呼び出しを行います。
まずgwt.xmlにてGINの呼び出しを行います。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.0.3//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.0.3/distro-source/core/src/gwt-module.dtd"> <module> <inherits name="com.google.gwt.user.User" /> <inherits name='com.google.gwt.user.theme.standard.Standard' /> <inherits name='org.slim3.gwt.emul.S3Emulation' /> <inherits name="com.google.gwt.activity.Activity"/> <inherits name="com.google.gwt.inject.Inject"/> <inherits name="com.google.gwt.logging.Logging" /> <inherits name="com.google.common.collect.Collect"/> <set-property name="gwt.logging.popupHandler" value="DISABLED"/> <script src="/js/jquery-1.4.2.min.js"/> <script src="/js/jquery-ui-1.8.1.custom.min.js"/> <source path="client"> <exclude name="**/*Test.java"/> </source> <source path="shared"> <exclude name="**/*Test.java"/> </source> <entry-point class="stk.client.Index"></entry-point> </module>
ちなみにcom.google.gwt.inject.InjectがGINの設定部分です。
次にGWTのモジュールクラスを記述します。
package stk.client; ~ import public class Index implements EntryPoint { Logger logger = Logger.getLogger(Index.class.getName()); @Override public void onModuleLoad() { final MyInjector injector = GWT.create(MyInjector.class); injector.getActivityManager(); Resources.INSTANCE.site().ensureInjected(); injector.getAuthService().loginAuth(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() { @Override public void onSuccess(LoginInfo result) { LoginInfo loginInfo = injector.getLoginInfo(); loginInfo.email = result.email; loginInfo.loggedIn = result.loggedIn; loginInfo.logoutUrl = result.logoutUrl; loginInfo.nickName = result.nickName; loginInfo.loginUrl = result.loginUrl; RootPanel.get("msg").setVisible(false); RootPanel.get("contents").add(injector.getContentsPanel()); injector.getHistoryHandler().handleCurrentHistory(); } @Override public void onFailure(Throwable caught) { } }); } }
今回の例ではログイン処理とかも入っていますが、注目していただきたいのは、
最初にGWT.createでMyInjectorを生成している部分です。
これによりMyInjectorが初期化されMyModuleなどが呼び出され、すべてのDI設定が行われます。
ちなみにinjector.getActivityManager();を次に呼び出しているのは、@Singletonアノテーションでは、
初期作成ではなく呼び出し時に初めてSingletonインスタンスが作成されるため、一度呼び出しを行っています。
多分もっとスマートにできる気がしますが。。。
またRootPanel.get("contents").add(injector.getContentsPanel());でRootPanelにフレームワークで利用する
ContentsPanel(@Named("contents")を指定したSimplePanel)を設定しています。
まとめ
非常にクラス数が多いため、めんどくさく感じるかもしれませんが、一度作ってしまえば、
前回の記事に比べほとんど変更は要らなくなります。
画面を増やす場合はPlaceとActivityを作成しinjectorにTokenizerメソッドを増やすだけでよくなります。
実際にMVP + GINを使っているサイトとして、
私がコミッターをやらさせていただいている、id:coolstyle さん(@yusuke_kokubo)のskillmapsがあります。
ソースコードもgithubで公開されていますので*4、是非ご覧になってみてください。
次回予告
次回は、MVPの話でなくて、GWT 2.1で追加されたCellViewあたりの話を書こうかと思います。たぶんいつか
あとは、MVP + GWTでのGWT.runAsyncのうまいやりかたを書こうかと たぶんいつか