Archives Wild Code School
Java Avancé

Design Pattern 01 : Singleton

Quête 5 sur 15

Quand tu développes une application Java, tu as parfois besoin d'avoir une persistance des données : un moyen de sauver les états de ton application. Par exemple :

  • l'utilisateur est-il connecté ?
  • si oui : quel est son nom, quel est le contenu de son panier ?

Tu vas découvrir dans cette quête le design pattern singleton, qui permet de résoudre cette problématique.

Un design pattern, ou patron de conception, est un ensemble de bonnes pratiques mises en place pour résoudre des problèmes récurrents de développement. Plutôt que d'inventer une nouvelle technique à chaque fois, il est plus sage de mettre en place des solutions éprouvées par des développeurs aguerris !

Objectifs

  • Comprendre le concept d'instance
  • Savoir créer et utiliser un singleton

Rappels

01. Instances et références

Avant de rentrer dans le vif du sujet, remémore-toi ce que sont une instance et une référence.

Pour t'aider, voici un schéma que tu as déjà vu dans une quête précédente :

java
Duck donald = new Duck("Donald", 84);

Ici donald est la référence d'une instance de la classe Duck.

Quand tu utilises le mot-clé new, tu appelles le constructeur de la classe, ici Duck(String name, int age) : cela crée un espace en mémoire contenant les données liées à l'instance. Ces données sont : les attributs, leurs valeurs, ainsi que les méthodes liées à l'instance.

Dans l'exemple, la variable donald est une référence. Elle permet d'accéder à une instance de la classe Duck.

02. Méthodes instanciées vs statiques

Maintenant que tu as ton instance et une référence pour y accéder, tu vas pouvoir appeler des méthodes dessus. Par exemple :

java
int age = donald.getAge(); // 84

La méthode getAge() est instanciée : elle s'exécute sur une instance de la classe Duck, pour en récupérer la valeur d'un de ses attributs. On pourrait imaginer d'autres méthodes, comme talk(), jump() ou sleep(), qui seraient liées à une instance de la classe.

Par opposition, une méthode statique ne sera pas liée à une instance de classe, mais à la classe elle-même.

Prends par exemple la classe Math : une méthode min(int a, int b) te permet de calculer le minimum entre deux entiers. Pour exécuter cette méthode, tu n'as pas besoin de faire d'instance de Math :

java
int min = Math.min(2, 3); // 2

Une méthode statique, du fait qu'elle n'est pas liée à une instance, ne peut pas interagir avec ses attributs.

↗
Static methods vs Instance methods in Java
geeksforgeeks.org

Scope

Prends le cas de la création de l'instance de User lors de sa connexion :

java
public class Account {

    public void signIn(String name) {
        User user = new User(name);
    }
}

Le scope (ou portée) de la variable user est limité à l'endroit où tu as initialisé l'instance : ici, la méthode signIn.

On peut résoudre ce problème en transformant user en attribut :

java
public class Account {

    private User user;

    public void signIn(String name) {
        user = new User(name);
    }

    public User getUser() {
        return user;
    }
}

Dans ce cas-là, le scope de user est étendu à l'instance de la classe Main, soit toutes les méthodes qu'elle contient. De plus, l'accesseur getUser permet d'étendre sa visibilité à l'extérieur.

↗
Variable Scope in Java
baeldung.com

Pattern Singleton

Il te reste encore à résoudre la problématique suivante : l'utilisateur sera lié à l'instance de la classe Account, et il est possible de créer plusieurs instances de la classe. Ce n'est pas logique, car à l'exécution de l'application, un seul compte utilisateur devrait exister !

java
Account account1 = new Account(); // crée une première instance
Account account2 = new Account(); // crée une seconde instance
// ici account1 != account2

Nous allons transformer la classe Account en singleton : ce design pattern limite la création d'un objet à une seule instance.

Tu peux lire ça de la façon suivante : si l'instance n'existe pas, elle sera créée, sinon l'instance déjà existante sera retournée. À tout moment, une seule instance sera disponible :

java
// there can be only one
private static Account instance;

public static Account getInstance() {  
    if (instance == null) {
        // if no instance exists, create one
        instance = new Account();
    }
    // in any case, return the same instance
    return instance;
}

La méthode getInstance est statique : elle ne s'exécutera pas sur une instance, mais directement sur la classe elle-même. Il ne faudra donc plus créer d'instance à partir du constructeur, mais à partir de l'appel de la méthode getInstance :

java
Account account1 = Account.getInstance(); // crée l'instance et la retourne
Account account2 = Account.getInstance(); // retourne la même instance
// ici account1 == account2

Par contre, en l'état, rien n'empêche le développeur de créer manuellement une instance avec le constructeur par défaut de Account :

java
Account account1 = Account.getInstance(); // crée l'instance et la retourne
Account account2 = new Account(); // crée une autre instance à partir du constructeur
// ici account1 != account2

Il faut donc empêcher le développeur de faire ça, en surchargeant le constructeur par défaut et en le rendant privé :

java
private Account() {}

Voici le code du singleton complet :

java
public class Account {

    // singleton declaration  
    private static Account instance;  

    public static Account getInstance() {  
        if (instance == null) {  
            // if no instance exists, create one  
            instance = new Account();  
        }  
        // in any case, return the same instance  
        return instance;  
    }  

    // private constructor to avoid new instances
    private  Account() {}

    // infos stored into the singleton  
    private User user;  

    public void signIn(String name) {  
        user = new User(name);  
    }  

    public User getUser() {  
        return user;  
    }  
}

Voici un exemple d'utilisation de ce singleton :

java
// in one process
Account account = Account.getInstance();  
account.signIn("Cartman");  

// later, in another process
Account account = Account.getInstance();  
User user = account.getUser();  
System.out.println(user.getName()); // Cartman

Sache que cette quête ne va pas aborder de notion de thread safe : rendre un singleton fonctionnel lors de l'utilisation de plusieurs threads. Si le sujet t'intéresse, tu trouveras des informations à ce sujet dans la ressource Double-Checked Locking with Singleton.

↗
Double-Checked Locking with Singleton
baeldung.com
↗
Singletons in Java
baeldung.com
↗
Singleton pattern
en.wikipedia.org

Démonstration

Maintenant que tu connais la théorie, va voir dans le dépôt en ressource un exemple d'utilisation d'un singleton.

Un utilisateur se trouve sur un site d'achat de meubles : il va se connecter, ajouter des produits au panier et, enfin, aller dans son panier.

La classe Main (que tu compileras et exécuteras) contient la connexion et navigation de l'utilisateur sur le site. Par contre, l'action d'ajout de meubles au panier sera dans la classe Shopping et l'affichage du panier sera dans la classe Basket.

Toutes les données sont stockées dans le singleton Account, qui contient un User, ce dernier possédant une liste de Furniture.

Trêve de bavardage, va voir comment tout cela s'articule !

Seule la classe Main est a compiler et exécuter.

↗
Dépôt d'utilisation d'un singleton
github.com

Challenge

Desperate Friend

Un ami développeur, désespéré, vient te voir : il doit créer une application permettant de retirer et ajouter de l'argent sur un compte bancaire, grâce à un singleton. À la fin du programme, il doit afficher le total du compte en banque.

Malheureusement, ton ami n'a pas bien compris comment les singletons marchent. Quand il exécute son code, il obtient le message d'erreur suivant :

java
Exception in thread "main" java.lang.NullPointerException
        at Singleton.withdraw(Singleton.java:21)
        at Transactions.transactions(Transactions.java:7)
        at Main.main(Main.java:8)

Un collègue de ton ami a commenté son code, afin de lui montrer que son implémentation de Singleton est mauvaise. De plus, il manque l'appel du singleton à l'affichage de la balance du compte.

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, Singleton, Bank et Transactions afin d'en étudier les contenus respectifs.
  2. Dans Singleton.java, modifie l'implémentation du singleton dans la méthode getInstance, afin de le rendre fonctionnel
  3. Dans Transactions.java, modifie la méthode balance afin d'appeler le singleton et récupérer et afficher le total du compte bancaire
  4. Compile et exécute la classe Main, afin de vérifier que le résultat est bien celui attendu

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

shell
$ Your balance is: 8720

Critères de validation

  • Seuls les fichiers Singleton.java et Transactions.java doivent avoir été modifiés.
  • La classe Main se compile sans erreur et affiche dans le terminal le même résultat que celui présenté précédemment.