Design Pattern 02 : Observer
Dans une application Java, tout ne s'exécute pas toujours de façon immédiate : quand un utilisateur clique sur "se connecter", le serveur va mettre un certain temps avant de répondre si ses email et mot de passe sont corrects.
Deux choix s'offrent à toi :
- soit tu bloques l'application jusqu'à avoir la réponse (au risque de planter si le retour tarde trop à venir)
- soit tu laisses l'application suivre son cours et tu agiras quand la réponse sera connue
C'est la seconde option que tu vas découvrir aujourd'hui, à l'aide du design pattern **observer** et plus spécifiquement des event listeners.
Objectifs
- Appréhender la notion d’asynchronisme
- Comprendre et utiliser des event listeners
Synchrone
Afin de comprendre un des intérêts des event listeners, il te faut d'abord bien saisir la différence entre des processus synchrones et asynchrones.
01. Définition
Un processus synchrone sera bloquant : la machine va attendre la fin de son exécution avant de passer à la suite. Par défaut, tout code est synchrone :
System.out.println(1);
try {
Thread.sleep(1000); // wait synchronously for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
Le résultat affiché sera :
$ 1
$ 2
Ici System.out.println(1) va se lancer tout de suite, puis Thread.sleep(1000) va bloquer le thread pendant 1000 ms, et enfin System.out.println(2) sera exécuté.
02. Exemple concret
Imaginons le cas où tu veux gérer la connexion d'un utilisateur : la personne entre son nom, l'application appelle la base de données, vérifie que l'utilisateur existe et enfin retourne les données liées à l'utilisateur.
Si tu fais un développement synchrone, tu peux bloquer l'exécution de l'application jusqu'à avoir la réponse de la base de données, créer une instance de User et la retourner.
On va simuler la connexion synchrone à la base de données avec un sleep qui dure entre 1 et 4 secondes (de façon aléatoire).
public static User signIn(String name) {
int time = (int) (Math.random() * ((4000 - 1000) + 1)) + 1000;
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new User(name);
}
Avantage :
- la méthode
signIn, même si elle va prendre un temps aléatoire, retourne une instance deUser
Inconvénient :
- l'exécution de l'application est bloquée. Imaginons que la base de données ne réponde qu'au bout d'une minute, il ne sera pas possible de faire quoi que ce soit d'autre pendant ce temps.
03. Conclusion
La plupart du temps, il n'y a pas de problème à faire un code synchrone. Cependant, il y a des cas où tu vas devoir trouver une autre solution :
-
si ton processus est très long à exécuter et cause une
TimeoutException -
si tu veux exécuter plusieurs processus en même temps
-
si tu ne connais pas le temps d'exécution du processus et, pire, s'il peut ne jamais aboutir
Asynchrone
01. Définition
Par opposition, un processus asynchrone est non bloquant : la machine enchaîne vers les processus suivants.
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println(1);
}
}, 1000); // wait asynchronously for 1 second
System.out.println(2);
Le résultat affiché sera :
$ 2
$ 1
Ici, Timer().schedule est un processus asynchrone : il attend 1000 ms avant de lancer sa méthode run.
La ligne System.out.println(2); sera donc lancée tout de suite et System.out.println(1); ne sera exécuté qu'une seconde plus tard.
02. Exemple concret
Reprenons le cas précédent de la connexion d'un utilisateur, de façon asynchrone.
On va simuler la connexion synchrone à la base de données avec un TimerTask, qui attend entre 1 et 4 secondes (de façon aléatoire) avant de lancer la méthode run.
public static void signIn(String name) {
int time = (int) (Math.random() * ((4000 - 1000) + 1)) + 1000;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
User user = new User(name);
}
}, time);
}
Avantage :
- l'appel de la méthode
signInne va pas bloquer l'exécution de l'application
Inconvénient :
- comme l'instance de
Userest créée en asynchrone, il n'est pas possible de la retourner par la méthodesignIn
Le résultat d'une méthode doit être renvoyé de façon synchrone.
03. Conclusion
C'est pratique, l’application n'est plus bloquée. Cependant, l'utilisation de l'asynchronisme empêche le retour du résultat par une méthode.
Pourtant, il serait important de savoir quand un utilisateur est connecté et de renvoyer ses données à la classe appelante. Pour réussir cet exploit, tu vas avoir besoin d'un event listener !
Pattern Observer
Nous allons créer un event listener afin d'écouter la méthode signIn et intervenir lorsque le résultat est connu. Un event listener est obtenu avec l'utilisation dérivée du design pattern observer.
Ce pattern est composé de trois acteurs :
-
l'événement
-
le sujet : c'est l'élément à observer, celui qui va produire une action et déclencher un événement.
-
l'observateur : c'est lui qui attend l'événement, afin de réagir en conséquence.
En Java, il existe plusieurs façons d'implémenter ce design pattern. Ici, nous allons voir l'event listener :
01. L'événement
Pour réaliser l'événement, tu vas avoir besoin de créer une interface qui contient les méthodes à appeler selon les diverses actions.
Pour le cas de la connexion utilisateur, on pourrait imaginer les états suivants :
-
onLoading: la connexion est en cours -
onSuccess: la connexion est terminée, retourne une instance deUser -
onError: une erreur est survenue, retourne le message d'erreur
public interface SignInListener {
/**
* the sign process is in progress
*/
void onLoading();
/**
* @param user returns the connected user
*/
void onSuccess(User user);
/**
* @param error returns the error message
*/
void onError(String error);
}
02. Le sujet
Afin de bénéficier des méthodes de cette interface, tu vas devoir l'ajouter en paramètre de la méthode à observer (le sujet), et appeler les événements selon ses actions :
public static void signIn(String name, SignInListener event) {
// loading begins
event.onLoading();
if (name == null || name.isEmpty()) {
// sends the error message and stops
event.onError("Name is null or empty");
return;
}
int time = (int) (Math.random() * ((4000 - 1000) + 1)) + 1000;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
User user = new User(name);
// sends the user
event.onSuccess(user);
}
}, time);
}
03. L'observateur
Le sujet ayant été modifié, il va falloir adapter l'observateur afin qu'il récupère l'événement.
Le sujet s'attend à une instance d'interface, il va falloir lui en fournir une... Une minute, il n'est pourtant pas possible de créer une instance d'une interface !
Pas de souci ! Java te permet de créer une classe anonyme, qui implémente une interface et en crée une instance en même temps :
Account.signIn("Cartman", new SignInListener() {
@Override
public void onLoading() {
System.out.println("Please wait...");
}
@Override
public void onSuccess(User user) {
System.out.printf("Welcome %s%n", user.getName());
}
@Override
public void onError(String error) {
System.out.printf("Sign in error : %s%n", error);
}
});
Le résultat sera le suivant :
$ Please wait...
$ Welcome Cartman
Le message Welcome Cartman apparaissant entre 1 et 4 secondes après le début du chargement.
Si tu changes "Cartman" par la valeur
nullou une chaîne vide, tu pourras lire un message d'erreur.
Challenge
Desperate Friend : Saison 2
Ton ami développeur est de nouveau dans tous ses états : son application bancaire ne fonctionne plus correctement !
Il y a encore quelques jours, il travaillait avec des données fictives, en synchrone : le montant était retiré et le résultat était renvoyé directement.
Désormais, il doit appeler le serveur bancaire et attendre le retour de ce dernier : cela prend du temps et il ne sait ni comment, ni quand afficher le solde.
Il a écrit le code suivant, qui crée un compte avec 1000€, retire 100€ puis affiche le solde du compte :
Bank account = new Bank(1000);
Transactions.withdraw(account, 100);
System.out.printf("Your balance is: %d%n", account.getTotal());
Cependant le résultat affiche "Your balance is: 1000", au lieu de "Your balance is: 900".
Un collègue de ton ami a commenté son code afin de l'aider. Cependant, ce dernier ne sait pas comment procéder et se repose encore une fois sur toi pour le dépanner !
Pour démarrer, fais un Fork du dépôt suivant puis clone-le en local.
Pense bien à faire un Fork, sinon tu ne pourras rien pousser !
- Ouvre les classes
Main,BanketTransactionsafin d'en étudier les contenus respectifs. - Crée une interface
TransactionListeneravec la signature de méthodevoid onComplete(Bank account); - Dans la classe
Transactions, modifie la méthodewithdrawafin d'y ajouter en argument une instance deTransactionListener. Ensuite, appelle la méthodeonCompleteau moment opportun. - Dans la classe
Main, modifie l'appel de la méthodewithdrawet passe en paramètre une classe anonyme deTransactionListener. Déplace l'affichage du solde du compte à l'endroit propice.
Résultat attendu lors de l'exécution de Main :
$ Your balance is: 900
Critères de validation
- Seuls les fichiers
Main.javaetTransactions.javadoivent avoir été modifiés. - Une interface
TransactionListener.javadoit avoir été créée. - La classe
Mainse compile sans erreur et affiche dans le terminal le résultat attendu.