Un peu de design de code avec le framework GWT – Part I

le 29/02/2008 par Mathieu Gandin
Tags: Software Engineering


GWT est un framework développé par Google permettant de réaliser des pages Web suivant la technologie AJAX. Ce framework propose de développer entièrement l'interface graphique à partir du langage Java.
Ce code est ensuite compilé en langage Javascript, pour être embarqué dans une application Web. GWT est composé d'une partie cliente, en Javascript, qui constitue l'IHM de l'application, elle communique avec une partie serveur développée en Java.

Pour autant, passé la découverte de cet excellent framework, une question se pose rapidement : Quels sont les bons patterns et designs de code à mettre en oeuvre avec ce framework ?
Partons d'un exemple, regardons les problèmes, et proposons une amélioration permettant de faire émerger un design cohérant.

Nous disposons d'un mini projet, constitué d'un écran de login. Cet exemple, volontairement simpliste, propose l'IHM suivante :

Pour arriver à ce résultat, il faut tout d'abord développer un écran en utilisant le framework GWT. Et après quelques exemples pour prendre en main GWT, on arrive très rapidement au résultat suivant :

public class LoginViewer implements EntryPoint {

private VerticalPanel panel;

private TextBox loginTextBox;

private PasswordTextBox passwordTextBox;

private Button button;

private User user;

public void onModuleLoad() {

RootPanel rootPanel = RootPanel.get();

user = new User();

user.setDateLogin(new Date());

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

String login = loginTextBox.getText();

String password = passwordTextBox.getText();

authenticate(login, password);

}

});

panel.add(button);

rootPanel.add(panel);

}

public void authenticate (String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

Window.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

Window.alert("Welcome !");

} else {

Window.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

user.setLogin(login);

user.setPassword(password);

user.setDateAuthentication(new Date());

rpc.isAuthenticated(user, callBackLogin);

}

A première vue, tout est dans une classe : la présentation, les appels aux serveurs, et quelques traitements pour vérifier si l'utilisateur est correctement authentifié et lui afficher des messages en conséquence. Ca fait beaucoup de responsabilités pour une seule classe ! On peut commencer par implémenter une classe de type Composite, LoginComposite, pour séparer le point d'entrée de l'application et l'écran d'authentification. Ainsi refactorer, le code de l'EntryPoint se retrouve bien allégé :

public class LoginViewer implements EntryPoint {

private LoginComposite composite;

public void onModuleLoad() {

RootPanel rootPanel = RootPanel.get();

composite = new LoginComposite();

rootPanel.add(composite);

}

}

Mais si on y regarde de plus prêt on n'a fait que déplacer la complexité dans la classe LoginComposite, et cette dernière demeure encore trop complexe. Remanions donc ce code en implémentant le pattern Command, via la classe LoginCommand. Cette classe est dédiée aux appels asynchrones entre la partie cliente et la partie serveur de l'application. Le code de notre composite, commence à se réduire de la manière suivante :

public LoginComposite()extends Composite {

commande = new LoginCommand();

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

String login = loginTextBox.getText();

String password = passwordTextBox.getText();

commande.authenticate(login, password);

}

});

panel.add(button);

initWidget(panel);

}

Et derrière ce la classe LoginCommand on retrouve le code suivant :

public class LoginCommand {

public void authenticate(String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

Window.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

Window.alert("Welcome !");

} else {

Window.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

User user = new User();

user.setLogin(login);

user.setPassword(password);

rpc.isAuthenticated(user, callBackLogin);

}

}

Mais voilà, un nouveau problème émerge dans le code que l'on vient de remanier : la classe LoginCommand dispose de code d'IHM avec les portions d'algorithme de type "Window.alert(...)". GWT est un framework AJAX, ce qui implique des communications asynchrones entre le client Javascript et le serveur, en Java. Pour récupérer le résultat, d'un traitement asynchrone et l'utiliser dans l'IHM, une bonne pratique est de définir un callback. Commençons par mettre en place une interface de callback, appelée LoginCallBack, dont le contrat consiste à lancer une alerte sur le résultat de l'appel du serveur :

public interface LoginCallBack {

void alert(String refresh);

}

Ensuite on remanie notre classe LoginCommand, pour remplacer le code d'IHM par le code de notre callback. La classe LoginCommand prend maintenant la forme suivante :

public class LoginCommand {

public void authenticate(final LoginCallBack callback,String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

callback.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

callback.alert("Welcome !");

} else {

callback.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

User user = new User();

user.setLogin(login);

user.setPassword(password);

rpc.isAuthenticated(user, callBackLogin);

}

}

Maintenant une question se pose au vu de notre refactoring : Comment relier ensemble les classes LoginComposite, LoginCommand, et LoginCallBack ? Avec une classe de contrôleur pardi ! On va créer une classe LoginController, qui implémente l'interface LoginCallBack. Elle se charge de faire la jonction entre LoginComposite et LoginCommand. La classe LoginController a la forme suivante :

public class LoginController implements LoginCallBack {

public LoginCommand loginCommand;

public LoginComposite composite;

public LoginController(LoginComposite composite) {

this.composite = composite;

this.loginCommand = new LoginCommand();

}

public void refresh() {

loginCommand.authenticate(this,composite.login, composite.password);

}

public void alert(String refresh) {

composite.alert(refresh);

}

}

Il faut maintenant finir de remanier la classe LoginComposite, pour qu'elle utilise notre contrôleur :

public class LoginComposite extends Composite {

private VerticalPanel panel;

private TextBox loginTextBox;

private PasswordTextBox passwordTextBox;

private Button button;

public String login;

public String password;

public LoginComposite() {

final LoginController controller = new LoginController(this);

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

login = loginTextBox.getText();

password = passwordTextBox.getText();

controller.refresh();

}

});

panel.add(button);

initWidget(panel);

}

public void alert(String message) {

Window.alert(message);

}

}

Et voilà ! Le design émergeant a du bon puisque nous venons d'implémenter le design pattern Modèle / Vue / Contrôleur avec le framework GWT. On a rajouté un mécanisme de Callback pour gérer le rafraîchissement de la vue, afin de gérer le modèle asynchrone de GWT. On pourrait aussi ajouter quelques interfaces, mais le code que l'on vient de remanier est suffisamment clair en terme de séparation des responsabilités.

On tient là notre première bonne pratique de design avec le framework, utiliser le pattern Modèle / Vue / Contrôleur, un classique pour le développement avec des frameworks d'IHM ...
D'autres pratiques seront à venir, mais en attendant si vous voulez recevoir le code complet de ce mini projet, demandez-le moi par mail à l'adresse suivante mga@octo.com.

A bit of design code with the framework GWT - Part I

GWT is a framework developed by Google to implement AJAX technology. This framework proposes to develop the GUI entirely from Java. This code is then compiled into Javascript, to be embedded in a web application. GWT is composed of a client part, Javascript, which is the GUI application, it communicates with a server developed in Java.

Past the discovery of this excellent framework, a question light in my mind: What are the correct patterns and designs code to implement this framework? Let's take an example, look at the problems and propose improvements to emerge a coherent design.

We have a Toy Project, which consists of a login screen. This example, deliberately simplistic, offers the following GUI:

To achieve this application, we must first develop a screen using the framework GWT. And after a few examples to take control of GWT, I code very quickly the following results:

public class LoginViewer implements EntryPoint {

private VerticalPanel panel;

private TextBox loginTextBox;

private PasswordTextBox passwordTextBox;

private Button button;

private User user;

public void onModuleLoad() {

RootPanel rootPanel = RootPanel.get();

user = new User();

user.setDateLogin(new Date());

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

String login = loginTextBox.getText();

String password = passwordTextBox.getText();

authenticate(login, password);

}

});

panel.add(button);

rootPanel.add(panel);

}

public void authenticate (String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

Window.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

Window.alert("Welcome !");

} else {

Window.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

user.setLogin(login);

user.setPassword(password);

user.setDateAuthentication(new Date());

rpc.isAuthenticated(user, callBackLogin);

}

At first sight, everything is in a class presentation, calls for servers, and some treatments to check if the user is properly authenticate. That's a lot of responsibility for one class!

We can start our refactoring by implementing a Composite class, LoginComposite to separate the entry point of the application and login screen. After Refactoring, the code is much lighter:

public class LoginViewer implements EntryPoint {

private LoginComposite composite;

public void onModuleLoad() {

RootPanel rootPanel = RootPanel.get();

composite = new LoginComposite();

rootPanel.add(composite);

}

}

But if we look more closely, we move the complexity in the class LoginComposite, and it is still too complex. Therefore we need to refactore the code by implementing the Command pattern, via the LoginCommand class. This class is dedicated to asynchronous calls between the client and the server. The code of the composite begins has been reduced in the following manner:

public LoginComposite()extends Composite {

commande = new LoginCommand();

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

String login = loginTextBox.getText();

String password = passwordTextBox.getText();

commande.authenticate(login, password);

}

});

panel.add(button);

initWidget(panel);

}

And behind the class LoginCommand we found the following code:

public class LoginCommand {

public void authenticate(String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

Window.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

Window.alert("Welcome !");

} else {

Window.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

User user = new User();

user.setLogin(login);

user.setPassword(password);

rpc.isAuthenticated(user, callBackLogin);

}

}

But now a new problem is emerging in this code: Class LoginCommand has GUI code, with some "Window.alert (...)".

GWT is an AJAX framework, which involves asynchronous communications between client and server Javascript, Java. To retrieve the result of an asynchronous processing and use in GUI, a good practice is to specify a callback. We begin by setting up a callback interface, called LoginCallBack, whose contract is to launch a warning about the outcome of the server:

public interface LoginCallBack {

void alert(String refresh);

}

Then we recast our class LoginCommand to replace the GUI code by the code of callback. The class LoginCommand now look like this:

public class LoginCommand {

public void authenticate(final LoginCallBack callback,String login, String password) {

AsyncCallback callBackLogin = new AsyncCallback() {

public void onFailure(Throwable caught) {

callback.alert(caught.getMessage());

}

public void onSuccess(Object result) {

User user = (User) result;

boolean isAuthenticated = user.isAuthenticate();

if (isAuthenticated) {

callback.alert("Welcome !");

} else {

callback.alert("Mauvais login");

}

}

};

LoginRPCAsync rpc = LoginRPC.Util.getInstance();

User user = new User();

user.setLogin(login);

user.setPassword(password);

rpc.isAuthenticated(user, callBackLogin);

}

}

Now a question arises in view of our refactoring: How do we relate all classes LoginComposite, LoginCommand and LoginCallBack? With a controller class of course !

Let's create a class LoginController, which implements the LoginCallBack. It works to match LoginComposite and LoginCommande. The class LoginController looks like this:

public class LoginController implements LoginCallBack {

public LoginCommand loginCommand;

public LoginComposite composite;

public LoginController(LoginComposite composite) {

this.composite = composite;

this.loginCommand = new LoginCommand();

}

public void refresh() {

loginCommand.authenticate(this,composite.login, composite.password);

}

public void alert(String refresh) {

composite.alert(refresh);

}

}

We must now complete our refactoring of LoginComposite class, now it uses our controller:

public class LoginComposite extends Composite {

private VerticalPanel panel;

private TextBox loginTextBox;

private PasswordTextBox passwordTextBox;

private Button button;

public String login;

public String password;

public LoginComposite() {

final LoginController controller = new LoginController(this);

panel = new VerticalPanel();

loginTextBox = new TextBox();

loginTextBox.setName("login");

panel.add(loginTextBox);

passwordTextBox = new PasswordTextBox();

passwordTextBox.setName("password");

panel.add(passwordTextBox);

button = new Button();

button.setText("log in !");

button.addClickListener(new ClickListener() {

public void onClick(Widget sender) {

login = loginTextBox.getText();

password = passwordTextBox.getText();

controller.refresh();

}

});

panel.add(button);

initWidget(panel);

}

public void alert(String message) {

Window.alert(message);

}

}

Et voilà! The emerging design was good, we have implemented the Model / View / Controller design pattern with GWT, plus a mechanism to manage Callback with asynchronous model of GWT. And then we take into our first practice with GWT, we use the Model / View / Controller Design Pattern, a classic for development with GUI framework...
Other practices will be coming, but in the meantime if you want to receive the complete code of this mini project, send me an email to mga@octo.com.