GWTでMVPアーキテクチャ その3 GINと愉快な仲間たち

明けましておめでとうございます。相変わらず読みにくい文章ですがよろしくお願い致します。
今年もあまり記事を書かなそうですが、頑張りたいと思います。

さてここ最近はひたすらappengine + gwt しかやっていないわけですが、
年末辺りにやったGWT のMVPアーキテクチャについて書きたいと思います。
毎回のとおり、翻訳能力が低いため、間違っている部分があるかと思いますが、見守っていただけたら幸いです。

ということで前回GWTのActivity + Place について記載しましたが、今回はそれにGWT用のDIコンテナGIN *1 を含めてより簡単にActivity + Placeを実現したいと思います。

GINとは

GINGWT用に作成されたDIコンテナ*2Google製の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のうまいやりかたを書こうかと たぶんいつか

追伸

文章がヘタクソで申し訳ございません。。。

*1:GIN http://code.google.com/p/google-gin/

*2:コンテナって言っていいのか微妙ですが

*3:っといってもspin up的にちょいと辛いですが。。。

*4:https://github.com/YusukeKokubo/SkillMaps