Zum Thema MVP (Model-View-Presenter) gibt es schon einiges an Dokumentation, etwa https://developers.google.com/web-toolkit/doc/latest/DevGuideMvpActivitiesAndPlaces, https://developers.google.com/web-toolkit/articles/mvp-architecture, doch soll dieser Beitrag einen weiteren Zugang zu dem Thema schaffen.
In einer MVP Anwendung macht die Sicht nur das, was sie machen soll: sie zeigt an. Sie ist zudem immer technologieabhängig und es kann verschiedene Implementierungen geben, etwa für GWT, Swing oder als Mock für Tests. Der Presenter ist mit der View assoziiert, aber da die View ja immer anders aussehen kann, kennt der Presenter sie nur über eine Schnittstelle. Damit beginnen wir (alle Programme im Pseudeocode):
interface MyView extends IsWidget {
void setName( String name );
String getName();
}
In der Schnittstelle kommen bis auf IsWidet keine GWT-spezifischen Eigenschaften nach außen, wobei es Entwickler gibt, die hier GWT-Schnittstellen wie HasText, IsWidget oder HasClickHandlers verwenden, dazu gleich mehr. Auch werden keine View-Model-Objekte (etwa Person) verwendet, sondern einfache Datentypen.
Die Implementierung für GWT kann den UiBinder nutzen oder nicht. Ohne sieht es etwa so aus:
class MyViewImpl extends Composite implements View {
private TextBox textbox;
MyViewImpl() {
initWidget( textbox );
}
@Override public void setName( String name ) { textbox.setValue( name ); }
@Override public String getName() { return textbox.getValue(); }
}
So einfach ist die View (die wir später noch ausbauen).
Nun kommt der Presenter, der die View initialisiert und auch die Interaktion mit dem Backend übernimmt. Wie stark er mit der View interagiert ist Geschmacksache.
Da der Presenter die View initialisiert und auf die View Einfluss nimmt, muss folglich der Presenter ein Verweis auf die View bekommen. Der Presenter merkt sich die View in einem Attribut:
class MyPresenter
{
private MyView myView;
…
}
Es gibt unterschiedliche Wege, wie der Presenter zur View kommt. Wenn man die View nicht austauschen möchte (und auch sonst einige Nachteile in Kauf nicht, siehe später), kann man den Presenter selbst die View erzeugen lassen. Oder man bietet einen Setter oder man lässt den Presenter injizieren. Die letzten beiden Wege erlauben den einfachen Austausch der View, etwa im Testfall. Außerdem muss die View natürlich an höher liegender Stelle irgendwie verfügbar sein, wenn zum Beispiel die MyView auf den Root-Panel gesetzt wird. Wenn man die View injiziert oder sie dem Presenter über einen Setter setzt, gibt es eine zentrale Stelle (ClientFactory in der GWT-Doku genannt) und von dort kann man diesen Teil-View für die jeweils darüber liegende View erfragen. Oder man fragt den Presenter selbst, wenn man sich so eine zentrale Stelle sparen möchte. In meinem Beispiel deklariere ich eine Methode getView() und mein Presenter erzeugt die View auch selbst:
class MyPresenter
{
private MyView myView = new MyViewImpl();
MyView getView() { return myView; }
}
Wenn man die View nun auf das Root-Panel setzen möchte, sieht das so aus:
MyPresenter presenter = new MyPresenter();
RootLayoutPanel.get().add( new ScrollPanel( presenter.getView() ) );
Einschub: Die Methode getView() kann man natürlich über eine neue Schnittstelle Presenter vorschreiben lassen, die etwa so aussehen kann:
public interface Presenter<T extends View> {
T getView();
}
In eigenen Projekten hatte ich das ursprünglich so entworfen, darin aber keinen Nutzen gefunden, und diese allgemeine Presenter-Schnittstelle wieder verworfen. Zudem hätte das auch eine View-Schnittstelle nötig gemacht, doch diese Abstraktion brachte mir nichts.
Erzeugt der Presenter die View muss man sich natürlich der Konsequenzen bewusst werden. Damit kann die View nicht mehr so einfach ausgetauscht werden (nur, wenn sie über eine Fabrik kommt), und es bringt auch den Nachteil mit, dass eine neuer Presenter-Instanz immer eine View neu erzeugt. Die View ist dann kein Singleton, die resettet und in nachfolgenden Presenter-Instanzen wiederverwendet wird. So wird jede neue Presenter-Instanz eine neue View-Instanz bilden. In meinem Beispiel soll das Ok sein, doch sollte man sich bei performanten GWT-Anwendungen im Klaren sein, die View nicht immer wieder neu zu erzeugen. Um eine zentrale ClientFactory kommt man nicht drum herum.
Im Grunde haben wir mit den drei Typen schon ein MVP realisiert:
- MyView: Schnittstelle mit Setter/Gettern zum Verändern/Auslesen der View
- MyViewImpl: Implementierung der View-Schnittstelle
- MyPresenter: Kennt die View, initialisiert sind und greift auf die Daten zurück. Kennt das wahre Model
Ist die View rein passiv reichen die drei Typen aus, doch das ist nur im Ausnahmefall so. Es fehlt ein ganz entscheidender Aspekt: Was machen wir, wenn die View Ereignisse auslöst? Drückt der Benutzer einen Button, so muss eine Logik angestoßen werden. Logik ist aber Teil des Presenters, nicht der View. Zwei Realisierungen bieten sich an.
Schaut man sich https://developers.google.com/web-toolkit/articles/mvp-architecture an, so wird ein GWT-spezifischer Typ wie HasClickHandlers von der View nach außen gereicht, sodass der Presenter Logik an diesen Handler/Listener hängen kann. Hat die View einen Button, so sieht das im Code etwa so aus:
interface MyView extends IsWidget {
void setName( String name );
String getName();
HasClickHandlers getOkButton();
}
Und die Implementierung:
class MyViewImpl extends Composite implements View {
private TextBox textbox;
private Button okButton;
@Override public HasClickHandlers getOkButton() { return okButton; }
…
}
Der Presenter holt sich von der View das HasClickHandlers-Objekt und hängt seine Logik daran:
class MyPresenter
{
private MyView myView = new MyViewImpl();
MyPresenter() {
myView.getOkButton().addClickHandler( new ClickHandler() { … } );
}
MyView getView() { return myView; }
}
Den Anmeldevorgang der Listener habe ich hier in den Konstruktor gesetzt, doch ist es empfehlenswert, sie in eine eigene (private) Methode bind() auszulagern.
Dieser Weg ist einfach und kostet verhältnismäßig wenig Code. Allerdings muss man das auch kritisch sehen, denn HasClickHandlers ist eine GWT-Schnittelle, genauso wie com.google.gwt.event.dom.client.ClickHandler. Möchte man eine View total von der Technologie unabhängig machen, so stören diese Typen, wobei es sehr angenehm ist, dass es Schnittstellen sind, und so auch von Swing/SWT/JSF im Prinzip umgesetzt werden können. Eine zweite Sache ist, dass man sich überlegen muss, wo man die Grenze bei den Typen zieht. Ein Textfeld zum Beispiel implementiert im Prinzip HasValue<String>, man kann also so weit gehen, auf View-Schnittstellen-Methoden wie
setValue(String)
String getValue()
zu verzichten und stattdessen so etwas wie
HasValue getValue()
zurückzugeben weil darüber ja ein setValue()/getValue() möglich ist.
Die neuen GWT-Beispiele etwa von https://developers.google.com/web-toolkit/doc/latest/DevGuideMvpActivitiesAndPlaces gehen einen anderen Weg und lassen die View selbst die Listener anhängen. Die View darf aber natürlich immer noch nicht die Logik ausführen, weshalb die View auf Methoden vom Presenter zugreifen kann. Damit gibt es eine bidirektionale Beziehung. Es wird mehr Arbeit als bei einer Lösung wie HasClickHandlers und das, was der View auf dem Presenter aufrufen möchte muss in einen neuen Typ fließen, denn die View soll ja nicht MyPresenter bekommen, sondern einen Basistyp.
Die Beschreibung der Presenter-Methoden kommt als innere Schnittstelle in die View-Schnittstelle und muss einen Presenter annehmen können:
interface MyView {
void setName( String name );
String getName();
void setPresenter( Presenter p );
interface Presenter {
ok();
}
}
Der Button selbst kommt nun nicht mehr nach außen, auf HasClickHandlers getOkButton() können wir verzichten.
Die View muss jetzt setPresenter() implementieren:
class MyViewImpl extends Composite implements View {
private TextBox textbox;
private Button okButton;
private Presenter presenter;
@Override public setPresenter( Presenter p ) { presenter = p; }
…
}
Mit dem Verweis auf den Presenter bekommt die View Zugriff auf die Logik von ok(), die immer dann aufgerufen werden soll, wenn der Button gedrückt wird. Die View wird ergänzt um die Ereignisbehandlung:
MyViewImpl() {
okButton.addClickHandler( new ClickHandler() {
presenter.ok();
} );
}
Das war es mit der View. Noch netter ist es natürlich, wenn man den UiBinder einsetzt, denn dann wird die Anmeldung noch etwas simpler:
@UiHandler("okButton") void onOkClick( ClickEvent e ) {
presenter.ok();
}
Der Presenter muss nur noch ok() implementieren, hat aber mit der Anhängen eines Listeneres nichts mehr zu tun:
class MyPresenter implements MyView.Presenter
{
private MyView myView = new MyViewImpl();
MyView getView() { return myView; }
@Override public void ok() { … };
}
Bewertung
Zielt man MVP voll durch, ersteht viel Code. In meinem Projekt hat eine View-Schnittstelle über 100 Methoden, die implementierende Klasse ist voll von kleinen Settern und Gettern. Schön ist das nicht. Wirklich vereinfachen kann man das nur dann, wenn man a) auf diese kleine Mini-Presenter-Schnittstelle in der View verzichtet und somit dem Presenter erlaubt, direkt die Listener anzumelden, und b) wenn man sich von dem schnittstellenorientierten Ansatz verabschiedet:
- Statt einer Schnittstelle MyView und einer Implementierung MyViewImpl schreibt man nur die eine konkrete View-Klasse und referenziert diese im Presenter direkt. Aus “private MyView myView;” wird also “private MyViewImpl myView;”. Setter/Getter können bleiben.
- Verschärfte Variante: Man verzichtet in der View auf die vielen Setter/Getter und greift in Presenter direkt auf die GWT-Widget zurück. Dann am Besten über die Schnittstellen um sich relativ unabhängig von den GWT-Klassentypen zu machen.
Wie weit man geht, ist jedem selbst überlassen.