GWTでMVPアーキテクチャ + EventBus + History その2(HistoryもAppControllerもさようなら)
ちょうど昨日頑張ってHisotryとEventBus、AppControllerの話を書いたのに
今日気がついたらGWT2.1.0-RC1が来ていて、AppControllerもHistoryもEventBusもいらなくなったよ
オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/
はい、意味ナッシングです。
でもいらなくなったのは必要なくなったのではなく、作らなくてよくなっただけなので、
昨日出て速攻でGWT2.1.0-RC1版MVPフレームワーク対応を行ってみました。
だって男の子だもん
てことで昨日と同じ感じで前提から
私は英語力も日本語力もないので、
わかりにくい、解釈が間違っている個所は多分にあるかと思います。
そういった場合はやさしく見守ってください。
今回MVP Frameworkを作る上で参考にしたのはGWTの公式サイトです。
Googleさん今回は非常に分かりやすかったです。
サンプルのソースが間違っている個所が少々ありましたが。。。
で中身はほぼ写経しました(めんどいところはコピペしました)。
コードはこちらです。
全体像などなど
昨日の記事を見て下さい。
もう一回書くと↓のような画面遷移です。
https://cacoo.com/diagrams/UmEpII6zMLeG4bRm
よくある
「一覧」→「新作成入力」→「一覧」
「一覧」→「修正入力」→「一覧」
のような遷移のモジュールを作成しています。
今回はこれをGWTでMVP化するとどうなるかを書きます。
登場人物
Activity
ActivityはMVPアーキテクチャで言うところのPresenterの役割を持ちます。
つまり責務は
- Eventの処理
- データバインディング
です。
GWT MVPフレームワークでは
Activity(MVPのPresenter)となるクラスはAbstractActivityを継承します。
そしてstart()メソッドで、担当するViewに対して、データバインドを行います。
コンストラクタで出てくるPlaceは後で解説します。
ClientFactoryは単純なFactoryです。
各getHogeにてインスタンスを作成し、返却します。
ListPresenter(Activity)
package com.google.code.stk.client.ui.presenter; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.client.ClientFactory; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.ListDisplay; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.event.shared.EventBus; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.AcceptsOneWidget; public class ListPresenter extends AbstractActivity implements ListDisplay.Presenter{ private final TwitterServiceAsync service; private final ClientFactory clientFactory; private final ListPlace place; private ListDisplay display; public ListPresenter(ListPlace place , ClientFactory clientFactory) { super(); this.place = place; this.clientFactory = clientFactory; this.service = this.clientFactory.getTwitterService(); } @Override public void clickNewButton() { clientFactory.getPlaceController().goTo(new NewPlace()); } @Override public void clickDeleteAnchor(final Key key) { service.delete(key, new AsyncCallback<Void>() { @Override public void onSuccess(Void arg0) { clientFactory.getPlaceController().goTo(new ListPlace("delete" , key.getId())); } @Override public void onFailure(Throwable throwable) { } }); } @Override public void clickEditAnchor(Key key) { clientFactory.getPlaceController().goTo(new EditPlace(key.getId())); } @Override public void savePinCode(String value) { service.registAccessToken(value, new AsyncCallback<Void>() { @Override public void onFailure(Throwable arg0) { } @Override public void onSuccess(Void arg0) { display.getPinCodeInputPanel().setVisible(false); display.getSavePinCodeButton().setEnabled(true); Window.alert("ピンコードを保存しました。"); } }); } @Override public void start(final AcceptsOneWidget panel, EventBus eventBus) { display = clientFactory.getListDisplay(); display.setPresenter(this); panel.setWidget(display); service.findAll(new AsyncCallback<List<AutoTweet>>() { @Override public void onSuccess(List<AutoTweet> arg0) { display.drowTable(arg0); } @Override public void onFailure(Throwable arg0) { Window.alert(arg0.getMessage()); } }); } }
ClientFactory(Factory)
ClientFactoryはDeferred Bindingを利用しGWT.createにて作成します。
これによりごっそりViewなどをモックに変更することが出来ます。Factory中のEventBusは昨日説明したEventBusとほぼ同じっぽいです。
つまり独自Eventを処理します。PlaceControllerは後ほど説明します。
package com.google.code.stk.client; import com.google.code.stk.client.service.TwitterService; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.AutoTweetDisplay; import com.google.code.stk.client.ui.display.AutoTweetView; import com.google.code.stk.client.ui.display.ListDisplay; import com.google.code.stk.client.ui.display.ListView; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.SimpleEventBus; import com.google.gwt.place.shared.PlaceController; public class ClientFactoryImpl implements ClientFactory { private ListDisplay listDisplay = null; private PlaceController placeController = null; private SimpleEventBus eventBus; private TwitterServiceAsync twitterService; @Override public AutoTweetDisplay getAutoTweetDisplay() { return new AutoTweetView(); } @Override public EventBus getEventBus() { if(eventBus == null){ eventBus = new SimpleEventBus(); } return eventBus; } @Override public ListDisplay getListDisplay() { if(listDisplay == null){ listDisplay = new ListView(); } return listDisplay; } @Override public PlaceController getPlaceController() { if(placeController == null){ placeController = new PlaceController(getEventBus()); } return placeController; } @Override public TwitterServiceAsync getTwitterService() { if(twitterService == null){ twitterService = GWT.create(TwitterService.class); } return twitterService; } }
Display(IsWidget)
MVPのDisplayは単純なViewクラスなのでWidgetやUiBinderあたりで作れば問題ありませんが、
Activityで利用する場合はIsWidgetインターフェースを継承しておくと、よいです。
Displayインターフェースの中にPresenterインターフェースを持っています。
これはビジネスロジックをPresenterに委譲させるために、
Displayにとって処理してほしいことをPresenterに切り出し、
そのPresenterを継承しているクラスをsetPresenterにて登録させ、
すべてのビジネスロジックをPresenterにもたせます。
これにより、ビジネスロジックと、画面が切り離され、
デスクトップアプリでありがちな、BigViewを防ぎます。
またそれぞれインターフェースとして、宣言させ、
それぞれのインターフェースを継承させることによって、
PresenterとDisplayの依存度が下がりテストのしやすさがまします。今回のListViewにとってのPresenterはListPresetnerです。
なのでListPresenterはListDislay.Presenterを継承し、
startメソッド内で、display.setPresenter(this)を行っています。そしてListView内の各@UiHanderのついたメソッドでビジネスロジック(データ登録など)を
Presenterに委譲しています。
ちなみにMVPの場合は画面遷移もPresenter側の仕事なのでPresenterに任せています。
Viewが行うのは自身のもつWidgetの管理のみです。
ListDisplay
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.IsWidget; public interface ListDisplay extends IsWidget{ void drowTable(List<AutoTweet> tweetList); public interface Presenter{ void clickNewButton(); void clickDeleteAnchor(Key key); void clickEditAnchor(Key key); void savePinCode(String value); } public void setPresenter(Presenter presenter); FlowPanel getPinCodeInputPanel(); Button getSavePinCodeButton(); }
ListView
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; public class ListView extends Composite implements ListDisplay { @UiField Button newButton; @UiField InlineLabel sizeLabel; @UiField FlexTable table; @UiField Anchor pinCodeLink; @UiField FlowPanel pinCodeInputPanel; @UiField TextBox pinCode; @UiField Button savePinCode; private static ListViewUiBinder uiBinder = GWT .create(ListViewUiBinder.class); private Presenter presenter; interface ListViewUiBinder extends UiBinder<Widget, ListView>{ } @Override public void drowTable(List<AutoTweet> tweetList) { table.removeAllRows(); if (tweetList == null || tweetList.isEmpty()) { table.setText(0, 0, "tweetなし"); return; } table.setText(0, 0, "ID"); table.setText(0, 1, "対象"); table.setText(0, 2, "内容"); table.setText(0, 3, "時間"); table.setText(0, 4, "間隔"); table.setText(0, 5, "ブレ"); table.setText(0, 6, "期間"); table.setText(0, 7, "最終Tweet日"); table.setText(0, 8, "修正"); table.setText(0, 9, "削除"); int row = 1; for (final AutoTweet autoTweet : tweetList) { table.getColumnFormatter().setWidth(1, "140em"); table.getColumnFormatter().setWidth(3, "6em"); table.getColumnFormatter().setWidth(4, "8em"); table.setCellPadding(10); table.setCellSpacing(10); table.setText(row, 0, String.valueOf(autoTweet.getKey().getId())); table.setText(row, 1, autoTweet.getScreenName()); table.setText(row, 2, autoTweet.getTweet()); table.setText(row, 3, autoTweet.getTweetHour() + "時"); table.setText(row, 4, autoTweet.getCycle().name()); table.setText(row, 5, autoTweet.getBure().name()); table.setText(row, 6, autoTweet.getStartMMdd() + " 〜 " + autoTweet.getEndMMdd()); table.setText(row, 7, autoTweet.getLastTweetAt()); Anchor syusei = new Anchor("修正"); syusei.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { presenter.clickEditAnchor(autoTweet.getKey()); } }); table.setWidget(row, 8, syusei); Anchor sakujo = new Anchor("削除"); sakujo.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { presenter.clickDeleteAnchor(autoTweet.getKey()); } }); table.setWidget(row, 9, sakujo); row++; } } @Override public Widget asWidget() { return this; } @Override public void setPresenter(Presenter presenter) { this.presenter = presenter; } @Override public FlowPanel getPinCodeInputPanel(){ return pinCodeInputPanel; } @UiHandler("newButton") void onClickNewButton(ClickEvent e){ presenter.clickNewButton(); } @UiHandler("pinCodeLink") public void onClickPinCodeLink(ClickEvent e){ pinCodeInputPanel.setVisible(true); } @UiHandler("savePinCode") public void onClickSavePinCode(ClickEvent e){ savePinCode.setEnabled(false); presenter.savePinCode(pinCode.getValue()); } public ListView() { initWidget(uiBinder.createAndBindUi(this)); } @Override public Button getSavePinCodeButton() { return savePinCode; } }
Place
Placeは画面や状態を表します。
昨日AppControllerとEventBus、Historyにてヒストリー中心の画面遷移を説明しました。
AppControllerとEventBus、Historyのやることをより、わかりやすく、そして責務を画面遷移、状態保持に
特化させたのがPlaceです。(タブン)
GWTでのPlaceフレームワークは画面や状態を表すPlaceクラスと、
それらを利用してHistoryイベントを発火するPlaceController、
PlaceとHistoryをマッピングさせるPlaceHistoryMapper、
そしてPlaceとActivityをひもづける、ActivityMapperを利用して実現します。
まず画面や状態を表す、Placeクラスの子クラスから
ListPlace
ListPlaceは一覧画面を表すPlaceクラスの子クラスです。
idやstateを持っているのは削除処理からもどった、
修正処理から戻った、
新規作成から戻ったなどを管理するためです。
ListPlaceにはListPlaceの情報からTokenを導き出す、
Tokenizerを持っています。
TokenizerはListPlaceの作成と、ListPlaceからtokenの作成を行います。
package com.google.code.stk.client.ui.place; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceTokenizer; public class ListPlace extends Place { private final String state; private final Long id; public ListPlace(){ this(null , null); } public ListPlace(String state, Long id) { this.state = state; this.id = id; } public ListPlace(String state) { this(state, null); } public Long getId() { return id; } public String getStringId(){ return id == null?"":id.toString(); } public String getState() { return state == null ? "" :state; } public static class Tokenizer implements PlaceTokenizer<ListPlace>{ @Override public ListPlace getPlace(String token) { if(token.contains("delete")){ return new ListPlace("delete" , Long.parseLong(token.replace("delete/", ""))); } if(token.contains("edit")){ return new ListPlace("edit" , Long.parseLong(token.replace("edit/", ""))); } if(token.contains("new")){ return new ListPlace("new"); } return new ListPlace(); } @Override public String getToken(ListPlace place) { return place.getState() + "/" + place.getStringId(); } } }
PlaceController
PlaceControllerはPlaceを利用して、PlaceHistoryChangeEventを発火し遷移を実現します。
たとえばListPresenterの下記メソッドは修正アンカー押下時に、
修正画面へ遷移させます。
ListPresenter
ListPresetner.java @Override public void clickEditAnchor(Key key) { clientFactory.getPlaceController().goTo(new EditPlace(key.getId())); }
PlaceHistoryMapper
PlaceHistoryMapperは各PlaceのTokenizerを利用して、HistoryTokenとPlaceのマッピングを行います。
@WithTokenizersアノテーションにTokenizerを指定するだけでマッピングしてくれます。
ちなみに発行されるHistoryTokenは {Place短縮クラス名}:{Tokenizer.getToken()}の形になります。。。
Place短縮クラス名がダサいので、治す方法が知りたい限りです。。。
package com.google.code.stk.client; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.gwt.place.shared.PlaceHistoryMapper; import com.google.gwt.place.shared.WithTokenizers; @WithTokenizers({ListPlace.Tokenizer.class , EditPlace.Tokenizer.class , NewPlace.Tokenizer.class}) public interface AppPlaceHistoryMapper extends PlaceHistoryMapper { }
ActivityMapper
ActivityMapperはPlaceとActivityをひもづけます。
ひもづけは頑張ります。。。
instanceofが微妙なのですが、、、
どうにかしたいですね。。。
package com.google.code.stk.client; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.code.stk.client.ui.presenter.EditPresenter; import com.google.code.stk.client.ui.presenter.ListPresenter; import com.google.code.stk.client.ui.presenter.NewPresenter; import com.google.gwt.activity.shared.Activity; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.place.shared.Place; public class AppActivityMapper implements ActivityMapper { private ClientFactory clientFactory; public AppActivityMapper(ClientFactory clientFactory) { super(); this.clientFactory = clientFactory; } @Override public Activity getActivity(Place place) { if(place instanceof ListPlace){ return new ListPresenter((ListPlace)place, clientFactory); } if(place instanceof NewPlace){ return new NewPresenter((NewPlace)place , clientFactory); } if(place instanceof EditPlace){ return new EditPresenter((EditPlace)place, clientFactory); } return null; } }
Activity、Placeまとめ
以上がActivity、Placeの関連クラスとその実装です。
画面遷移をHistory中心で行うことが出来、ブラウザBack、Ajaxにつよりアプリケーションが作成できます。
昨日書いた、AppControllerや、Historyはフレームワークの中に隠れた状態になっています。
(または各MapperやPlaceに分散されている)
上記の各MapperクラスはEntryPointあたりで各Manegerクラスに登録しておきます
ClientFactory clientFactory = GWT.create(ClientFactory.class); EventBus eventBus = clientFactory.getEventBus(); PlaceController placeController = clientFactory.getPlaceController(); // Start ActivityManager for the main widget with our ActivityMapper ActivityMapper activityMapper = new AppActivityMapper(clientFactory); ActivityManager activityManager = new ActivityManager(activityMapper,eventBus); activityManager.setDisplay(appWidget); // Start PlaceHistoryHandler with our PlaceHistoryMapper AppPlaceHistoryMapper historyMapper = GWT .create(AppPlaceHistoryMapper.class); PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper); historyHandler.register(placeController, eventBus, defaultPlace);
もうチョイ細かい動きはまた今度追います。
あとがき
今回のも前回のも全くMVPフレームワークのデザインパターンを説明してない。。。
なんでHogeDisplayがPresetner持ってるのとか説明しないとですね。。。
でもAcitivityとPlaceが出ちゃったんだもん。。。
うん、良かったと思ってるよ。。。
Data Presenterも書きたいですね。