Depuis la conférence
Google I/O 2009, le pattern architectural MVP fait parler de lui à un point où
un grand nombre de framework open source s’est répandu, chacun proposant son
implémentation plus ou moins élaborée. Tous contiennent au moins la gestion du
présenteur et ensuite on y trouve ou non un event bus, une gestion de
l’historique, injection de dépendances, pattern action, ....
C’est sur
google code que j’ai découvert Mvp4g qui
couvre une bonne partie de ses fonctionnalités.
Pour vous le
faire découvrir, nous allons reprendre le code de l’application de gestion de
contacts de l’article de google sur le mvp : http://code.google.com/intl/fr/webtoolkit/articles/mvp-architecture.html
Pour
cet exercice, nous
prendrons la dernière version de
mvp4g qui vient l’être releasée ainsi que de ses dépendances :
Comme pour
n’importe quel bibliothèque gwt, il faut la déclarer dans notre fichier gwt.xml
:
|
<inherits
name='com.mvp4g.Mvp4gModule' />
|
Maintenant,
faisons un tour d’horizon des modifications que nous allons devoir apporter
:
-
La partie Model ne changera pas : Mvp4g ne couvrant pas l’Action pattern, nous
ferons appel au méthodes exposée en rpc tout comme dans le code original.
-
La partie Presenter, elle sera une des parties qui changera le plus : nous
devrons étendre une classe du framework et répondre à son contrat.
-
La partie View ne changera que très peu : nous n’aurons qu’à changer
l’interface implémentée.
-
L’Event Bus : un des autres gros changement. Mvp4g propose en effet son propre
modèle avec un système d’évènements haut niveau assez souple.
-
La gestion de l’historique, là aussi tout est à refaire.
-
L’Entry Point : Avec mvp4g, nous n’avons pas forcément besoin d’en écrire un
...
A cela, nous
rajouterons une injection de dépendances via gin qui n’est pas dans le code
original. C’est aussi dans cet esprit que se configure mvp4g. Que ce soit pour
lier les différents composants ou spécifier le comportement dus bus ou de
l’historique. Elle peut être soit réalisée en xml ou par annotations. J’ai
préféré l’utilisation des annotations.
J’imagine que
le point sur l’entry point, vous a laissé perplexe mais attention ce n’est pas
parce que nous n’allons pas en écrire un que nous n’en aurons pas. C’est
justement parce que l’application s’assemble en fonction des annotations que
cette partie peut devenir générique. Nous utiliserons donc celui que nous
propose mvp4g en incluant cette ligne dans le gwt.xml :
|
<entry-point
class='com.mvp4g.client.Mvp4gEntryPoint' />
|
Pour les plus
curieux, voici le code qui y correspond :
|
Mvp4gModule
module = (Mvp4gModule)GWT.create( Mvp4gModule.class );
module.createAndStartModule();
RootPanel.get().add(
(Widget)module.getStartView() );
|
Et c’est tout.
Le code étant assez simple, il se comprend de lui même. Tant que nous n’aurons
qu’un seul module gwt, il nous sera suffisant.
Passons
maintenant au centre névralgique de notre application : le bus évènementiel.
Dans une application mvp4g, il est indispensable. C’est bien sûr par lui va
passer la distribution des événements haut niveau mais aussi quelques autres
notions liées comme la gestion de l’historique et surtout l’action
“start”.
A 3ème ligne
de l’entry point, vous aviez sans doute remarque le getStartView. Il n’y a rien
de magique, c’est par la configuration du bus que nous la
connaîtrons.
Allez, mettons
nous en route , pour coder notre bus. En fait, nous n’aurions rien à
implémenter puisqu’il s’agit d’une interface. Elle devra néanmoins étendre
l’interface EventBus de mvp4g et être annotée :
|
@Events(
startView = RootView.class)
public interface
ContactsEventBus extends EventBus {
}
|
C’est sur
l’annotation @Events que nous définissons RootView en tant que vue chargée
s’affichant au démarrage. Fidèle à son nom, elle sera notre vue racine. Mvp4g
conseille de disposer d’une telle vue qui servira de container aux autres vues.
Comme toute vue dans un modèle MPV, elle sera statique et sera associée à un
présenteur. L’interface qui servira de contart avec le présenteur sera celle ci
:
|
public interface RootViewInterface
{
Panel getBody();
}
|
et
l’implémentation sera la suivante :
|
public class RootView
extends Composite
implements
RootViewInterface
{
private SimplePanel
body = new SimplePanel();
public RootView()
{
VerticalPanel mainPanel = new VerticalPanel();
mainPanel.add(body);
initWidget(mainPanel);
}
@Override
public Panel
getBody() {
return body;
}
}
|
Le présenteur
quant à lui sera de la forme suivante :
|
@Presenter(
view = RootView.class )
public class RootPresenter
extends BasePresenter<RootViewInterface,
ContactsEventBus>
|
Là encore,
nous avons du code assez simple à comprendre . Nous annotons le présenteur pour
qu’il puisse être détecté par mvp4g et étendons la classe BasePresenter<V, E
extends EventBus> pour nous permettre d’interagir avec la vue mais aussi
avec notre bus. De la même manière, nous aurons : un ContactsPresenter et un
EditContactPresenter avec des vue telle qu’on les a dans le code
original.
Nous avons
déjà la base de notre application mais pour l’instant, elle ne fait rien. Dans
notre cas, nous voudrions qu’au lancement, la liste des contacts soit affiché.
Tiens, nous tenons un évènement haut niveau et nous voudrions qu’il soit lancé
au lancement... C’est encore une fois le bus qui va s’en occuper, encore un
fois par une simple annotation.
Puisque nous
voulons lister, nous allons créer un évènement “list” tout simplement. A la
différence du mvp proposé par google dans l’article, nous n’aurons pas à créer
une classe héritant de GwtEvent et de lui associer une handler. Nous allons
seulement ajouter une méthode qui porte le nom de cet évènement à l’interface
de notre bus :
|
@Start
@Event(handlers
= ContactsPresenter.class)
void list();
|
C’est grâce à
l’annotation @Start que l’évènement sera lancé au lancement client de
l’application et sera délégué à notre ContactPresenter. A noter que dans notre
cas, nous n’avons qu’un seul handler. Il aurait été tout à fait possible d’en
avoir plusieurs en les séparant par une virgule.
Il nous reste
à implémenter la réaction du présenteur face à cet évènement. Le modèle
évènementiel de mvp4g repose sur une convention simple : pour un méthode XXX du
bus, le présenteur devra implémenter une méthode onXXX tout simplement. Bien
qu’il ne soit pas possible à s’assurer de la concordance entre l’évènement est
sa méthode réceptrice lors de la compilation java, ce contrôle est réalisé lors
de la phase de compilation gwt.
Revenons à
notre cas, nous aurons donc une méthode onList() dans notre ContactsPresenter
:
|
public void onList(){
contactsService.getContactDetails(newAsyncCallback<ArrayList<ContactDetails>>()
{
@Override
public void onSuccess(ArrayList<ContactDetails>
result) {
doList(result);
}
@Override
public void onFailure(Throwable
caught) {
eventBus.errorMessage(ErrorMessages.RPC_CALL_FAILED);
}
});
eventBus.changeBody(view.asWidget());
}
|
Pour
déclencher un évènement haut niveau, il suffit d’appeler la méthode associée du
bus.C’est ce que
nous faisons par exemple avec le changeBody qui permet à la vue ContactView de
devenir active.
Nous pouvons
aussi transmettre une valeur ou plusieurs valeurs lors du déclenchement de
l’évènement. Nous rencontrons ce cas pour l’édition :
|
@Event(handlers
= EditContactPresenter.class)
void edit(String id);
|
Vous avez
aussi sûrement remarqué, l’appel au service rpc. Sa déclaration est très
simple, nous utiliserons l’injection GIN par une simple annotation sur son
setter :
|
@Inject
public void
setContactsService(ContactsServiceAsync
contactsService) {
this.contactsService =
contactsService;
}
|
Quant à la
phase de binding des événements bas niveau par les présenteurs, elle est est
analogue à celle du code original : nous n’aurons qu’à implémenter la méthode
bind().
Nous aurons
par exemple :
|
view.getAddButton().addClickHandler(new
ClickHandler() {
public void onClick(ClickEvent
event) {
eventBus.add();
}
});
|
Si vous avez
peur de ne pas savoir où vont vos évènements, l’annotation @Debug sur le bus
vous aidera en loggant en console ce qui se passe autour du bus. Il existe 2
modes de LogLevel : SIMPLE et DETAILLED, par défaut ce sera la première valeur.
Regardons ce qui se passe dans les 2 modes sur un évènement edit .
Tout d’abord
en mode simple :
16:47:05.355
[INFO] [contactsmvp4g] Module: Mvp4gModule || event: edit || param(s):
18
16:47:06.551
[INFO] [contactsmvp4g] Module: Mvp4gModule || event: changeBody || param(s):
[...plein de html...]
et en mode
détaillé :
16:50:53.187
[INFO] [contactsmvp4g] Module: Mvp4gModule || event: edit || param(s):
18
16:50:53.751
[INFO] [contactsmvp4g]
com.sfeir.contacts.client.presenter.EditContactPresenter@640782 handles
edit
16:50:54.052
[INFO] [contactsmvp4g] Module: Mvp4gModule || event: changeBody || param(s):
[...plein de html...]
16:50:54.058
[INFO] [contactsmvp4g]
com.sfeir.contacts.client.presenter.RootPresenter@16e4f00 handles
changeBody
Intéressons
nous maintenant à l’historique. Mvp4g utilise la notion de convertisseur
d’historique. Ce qui signifie que nous devrons implémenter la façon dont les
évènements seront convertis en url et vice et versa. Nous allons devoir
implémenter l’interface HistoryConverter<E extends EventBus> et comme
toujours l’annoter. Notre application étant relativement simple, nous n’en
aurons qu’un seul mais sachez qu’il est tout à faire possible d’en avoir
plusieurs. Pour répondre à son contrat, nous devrons implémenter les 2 méthodes
suivantes :
- convertFromToken( String eventType, String param, E eventBus ) : réaction
lors d’une url tokenisée.
- isCrawlable() : indique si le l’url sera crawlable en renvoyant un
booléen.
La gestion de
l’historique viendra se greffer sur le bus événementiel. Pour gérer
l’historique, nous rajouterons :
|
@Events(
startView = RootView.class, historyOnStart
= true)
|
Réfléchissons
d’abord un peu. Sur quoi aimerions nous avoir une historique avec des urls
identifiables ? J’en vois 3 :
Le modèle
d’historique de mvp4g utilise un token situé après un "#" dans l’url. Ce token
correspond tout simplement au nom de l’évènement. Nous avons aussi la
possibilité d’assigner un valeur à ce dernière en le placant derrière un
"?".
Exemple :
#token?value
Cette solution convient à notre
exigeance de mieux marquer l’url lors de l’édition d’un
contact.
Pour lier, un
évènement à notre convertisseur d’historique, il suffit de lui assigner sur le
bus :
|
@Start
@InitHistory
@Event(handlers
= ContactsPresenter.class, historyConverter=ContactHistoryConverter.class)
void list();
|
L’annotation
@InitHistory sert à marquer quand l’historique doit être initialisé. Celle est
obligatoire si vous voulez utiliser l’historique. Revenons à notre
convertisseur :
|
@History
public class ContactHistoryConverter
implements HistoryConverter<ContactsEventBus>
{
@Override
public void convertFromToken(String eventType,
String param,
ContactsEventBus eventBus) {
if(“edit”.equals(eventType)){
eventBus.edit(param);
}
else if(“add”.ADD.equals(eventType)){
eventBus.add();
}
else if(“list”.equals(eventType)){
eventBus.list();
}
}
@Override
public boolean isCrawlable()
{
return true;
}
}
|
La première
méthode est assez claire : nous appelons la méthode correspondante au token
avec une particularité pour edit où nous nous servirons de la valeur en
paramètre du token comme paramètre edit.
Même si l’IDE
ne signale aucune erreur, la compilation gwt en provoquera. Il nous faut la
réciproque de la méthode convertFromToken : c’est à dire indiquer quelle sera
la valeur du paramètre d’un token. Ainsi, pour chaque évènement XXX
historisé,il
nous faudra une méthode onXXX qui renvoi une valeur sous forme de chaîne de
caractères. Nous aurons ainsi par exemple :
|
public String onEdit(String id){
return id;
}
public String onList(){
return "";
}
|
Un reproche
possible à faire à mvp4g est
Un des
reproches possible à faire est l’assemble dynamique et instanciation des
composants. Nous n’avons pas toujours besoin d’en disposer au moment au l’ont
accède à l’application. Dans notre cas, nous n’avons pas besoin d’édition d’un
contact tant que n’a pas cliqué sur un contact. Rassurez vous, la construction
paresseuse est là. Pour cela, nous allons changer la super classe pour
LazyPresenter<V extends LazyView, E extends EventBus>. En plus de cela,
notre interface de vue devra étendre l’interface LazyView. Cela va perturber un
peu notre code et des erreurs de compilation vont s’inviter. Avant de les
corriger, intéressons nous à la classe LazyPresenter et en particulier sa
méthode bind()
|
final public void bind()
{
view.createView();
createPresenter();
bindView();
}
|
Voilà la clé
de nos corrections :
Personnellement,
j’ai remarqué qu’après ses modifications, le lancement était plus
rapide.
Le principal
avantage d’un découpage mvp est la testabilité. Notre présenteur étant décorélé
de problématique d’affichage, il sera alors plus simple de le tester. En effet,
en mockant la vue, les services et le bus, nous pourrons nous concentrer nos
tests sur la logique métier. De plus avec le système évènementiel haut niveau
de mvp4g, il est simple de tester la réception d’un événement.
Un aspect qui
peu rebuter les plus intégriste du modèle mvp est la gestion évènement de haut
niveau spécifique de mvp4g et principalement ses performances. Quelles sont
elles ? Le wiki du projet répond à cette question :
http://code.google.com/p/mvp4g/wiki/Mvp4gPerformances
Les
performances semble honorables. Bien sûr, tout produit vante ses performances,
mais bon joueur les tests sont disponibles.
La question à
se poser avant tout est de savoir ce que l’on teste exactement. En observant le
code, on voit qu’un évènement est lancé est attrapé par son réceptacle. Rien de
plus. Pour éviter un conflit entre les deux, un test se fait en temps : d’abord
mgp4g puis gwtEvent.
Pour la partie
benchmarking, j’ai réalisé depuis le lien appspot les même tests sous 3
navigateurs dans leur version courante :
-
Chrome
6
-
Internet
Explorer 8
-
Firefox
3.6
Mes résultats
diffèrent, mais attention cela ne signifie pas les résultats sur la page mvp4g
sont faux. En effet, pour avoir reproduit ces même tests sur différentes
machines possédant le même navigateur, j’obtenais des résultats avec une
différence assez importante (jusqu’à x 10 entre un bon pc fixe et un simple
netbook) :
Voici mes
résultats pour 1000 évènements lancé :
|
|
mvp4g
|
gwt
|
|
Chrome 6
|
31
|
122
|
|
Internet Explorer
8
|
477
|
3609
|
|
Firefox 3.6
|
206
|
312
|
Ces résultats vont dans le
même sens, c’est à dire que le modèle événementiel de mvp4g est plus rapide que
celui de gwt.
Bien sûr, GWT
2.1 va arriver avec son modèle MVP intégré. Quand il sortira, on pourra
re-comparer les temps de réaction événementielle. En attendant, sa sortie vous
pouvez vous amuser au mpv avec mpv4g, qui propose bien d’autres fonctionnalités
encore comme les filtres d’évènements, la gestion du multi-module,
....
Le code source
complet est joint à cet article.