Archives Wild Code School
Java Avancé

Design Pattern 02 : Observer

Quête 6 sur 15

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 :

java
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 :

bash
$ 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).

java
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 de User

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.

java
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 :

bash
$ 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.

java
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 signIn ne va pas bloquer l'exécution de l'application

Inconvénient :

  • comme l'instance de User est créée en asynchrone, il n'est pas possible de la retourner par la méthode signIn

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 !

↗
Quelle est la différence entre des exécutions synchrone et asynchrone ?
fr.quora.com

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 de User

  • onError : une erreur est survenue, retourne le message d'erreur

java
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 :

java
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 :

java
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 :

bash
$ 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 null ou une chaîne vide, tu pourras lire un message d'erreur.

↗
Dépôt d'utilisation d'un event listener
github.com
↗
Creating Custom Listeners
guides.codepath.com
↗
Observer pattern
en.wikipedia.org

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 :

java
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 !

  1. Ouvre les classes Main, Bank et Transactions afin d'en étudier les contenus respectifs.
  2. Crée une interface TransactionListener avec la signature de méthode void onComplete(Bank account);
  3. Dans la classe Transactions, modifie la méthode withdraw afin d'y ajouter en argument une instance de TransactionListener. Ensuite, appelle la méthode onComplete au moment opportun.
  4. Dans la classe Main, modifie l'appel de la méthode withdraw et passe en paramètre une classe anonyme de TransactionListener. Déplace l'affichage du solde du compte à l'endroit propice.

Résultat attendu lors de l'exécution de Main :

shell
$ Your balance is: 900

Critères de validation

  • Seuls les fichiers Main.java et Transactions.java doivent avoir été modifiés.
  • Une interface TransactionListener.java doit avoir été créée.
  • La classe Main se compile sans erreur et affiche dans le terminal le résultat attendu.