Design Pattern 01 : Singleton
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 :
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 :
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 :
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.
Scope
Prends le cas de la création de l'instance de User lors de sa connexion :
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 :
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.
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 !
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 :
// 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 :
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 :
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é :
private Account() {}
Voici le code du singleton complet :
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 :
// 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.
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
Mainest a compiler et exécuter.
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 :
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 !
- Ouvre les classes
Main,Singleton,BanketTransactionsafin d'en étudier les contenus respectifs. - Dans
Singleton.java, modifie l'implémentation du singleton dans la méthodegetInstance, afin de le rendre fonctionnel - Dans
Transactions.java, modifie la méthodebalanceafin d'appeler le singleton et récupérer et afficher le total du compte bancaire - 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 :
$ Your balance is: 8720
Critères de validation
- Seuls les fichiers
Singleton.javaetTransactions.javadoivent avoir été modifiés. - La classe
Mainse compile sans erreur et affiche dans le terminal le même résultat que celui présenté précédemment.