L’objectif de ce patron de conception est de garantir qu’une application ne peut créer qu’une seule instance d’une classe donnée. On pourrait imaginer que l’on ne souhaite créer qu’une seule instance de notre Factory parce qu’elle conserve en mémoire un « pool » de personnages, qu’elle utilise chaque fois qu’on lui demande d’en construire un nouveau.
Définition
Le design pattern Singleton permet de garantir qu’une unique instance d’une classe donnée pourra être créée dans l’application. Si on essaye de créer deux instances, on récupérera la même.
Tout d’abord, il faut empêcher que n’importe qui puisse utiliser le ou les constructeur(s) de la classe. On va donc les mettre en private. Ensuite, pour pouvoir tout de même créer des instances, on va utiliser une méthode public static. En UML, cela correspond au diagramme de classes suivant :
En termes de programmation, on aurait un code similaire à :
public class MaClasse {
// l'unique instance qui sera créée
private static MaClasse instance;
// les constructeurs en private
private MaClasse() {
System.out.println("construction d'une instance de MaClasse");
}
// les méthodes de la classe
public String toString() {
return "MaClasse";
}
// la méthode qui va être appelée pour construire toutes les instances
public static MaClasse getInstance() {
if (instance == null) {
instance = new MaClasse();
}
return instance;
}
public static void main(String[] args) {
MaClasse maClasse1 = MaClasse.getInstance();
MaClasse maClasse2 = MaClasse.getInstance();
}
}
Dans le code ci-dessus, maClasse1 et maClasse2 référencent le même objet.
Attention
Il faut noter que le code ci-dessus n’est pas adapté pour le multithreading. En effet, rien n’empêche une thread appelant getInstance() d’être interrompue après avoir passé le if mais avant d’avoir affecté l’attribut instance. Dans ce cas, une autre thread peut appeler getInstance(), passer le if et affecter instance, puis la première thread peut reprendre son exécution et affecter à nouveau instance.
Une solution possible en Java pour contourner le problème du multithreading consiste à adapter le code ci-dessus en celui ci-dessous. On crée directement l’instance avant d’appeler la méthode getInstance(). Java garantit qu’on ne peut accéder à un objet d’une classe avant que la classe soit complètement chargée. Du coup, la 3ème ligne est exécutée avant qu’on puisse faire le return instance.
1public class MaThreadedClasse {
2 // l'unique instance qui sera créée
3 private static MaThreadedClasse instance = new MaThreadedClasse();
4
5 // les constructeurs en private
6 private MaThreadedClasse() {
7 System.out.println("construction d'une instance de MaThreadedClasse");
8 }
9
10 // les méthodes de la classe
11 public String toString() {
12 return "MaThreadedClasse";
13 }
14
15 // la méthode qui va être appelée pour construire toutes les instances
16 public static MaThreadedClasse getInstance() {
17 return instance;
18 }
19
20
21 public static void main(String[] args) {
22 MaThreadedClasse maClasse1 = MaThreadedClasse.getInstance();
23 MaThreadedClasse maClasse2 = MaThreadedClasse.getInstance();
24 }
25}
Notez qu’en C++, on trouve souvent sur internet des codes de Singleton similaires à celui de MaClasse.java. Ces codes ne sont pas thread-safe. En C++, il faut utiliser le Singleton de Meyers qui a en plus le bon goût d’être plus simple :
#include <iostream>
class MaThreadedClasse {
private:
// rendre les constructeurs et destructeurs privés
MaThreadedClasse() = default;
~MaThreadedClasse() = default;
public:
// empêcher les constructeurs/opérateurs de copie et de move
MaThreadedClasse(const MaThreadedClasse&) = delete;
MaThreadedClasse(MaThreadedClasse&&) = delete;
MaThreadedClasse& operator = (const MaThreadedClasse&) = delete;
MaThreadedClasse& operator = (MaThreadedClasse&&) = delete;
// la méthode pour créer les instance
static MaThreadedClasse& getInstance() {
static MaThreadedClasse instance;
return instance;
}
};
Les variables statiques définies dans une portée locale (à l’intérieur d’une fonction par exemple) sont créées quand elles sont utilisées la première fois. Donc C++ garantit que c’est la première thread qui accèdera à instance qui la créera. Et cette opération est bloquante : aucune autre thread ne pourra essayer de la créer.
Avantages :
On a la garantie qu’une seule instance est créée dans tout le programme.
On peut accéder à cette instance facilement en utilisant autant de fois que l’on veut la méthode getInstance().
Les version multithreadées sont « efficaces », c’est-à-dire qu’elles n’utilisent pas de mécanisme de synchronisation (lock).
Inconvénients :
Chaque fois que l’on appelle getInstance(), on obtient la même instance. Le programme doit donc tenir compte du fait que toutes ces instances sont « partagées », en particulier quand on travaille en multithreading.
Singleton empêche de faire de l’héritage dans la mesure où les constructeurs sont privés, pas protected.
Utilisez le patron Singleton pour garantir qu’une seule interface utilisateur (UI) est créée dans votre application de gestion de l’USS Orville.
Considérons le code d’une classe BandeEnnemis qui permet de stocker au sein d’un même objet un ensemble d’ennemis :
package fr.polytech.aco.jeu.ennemi;
import fr.polytech.aco.MyArray;
public class BandeEnnemis {
private MyArray<Ennemi> ennemis;
public BandeEnnemis() {
this.ennemis = new MyArray<>(2);
}
public BandeEnnemis(Ennemi[] ennemis) {
this.ennemis = new MyArray<>(ennemis);
}
public void addEnnemi(Ennemi ennemi) {
this.ennemis.pushBack(ennemi);
}
public String attaquer() {
String res = "";
for (var ennemi : this.ennemis)
res += ennemi.attaquer() + " ; ";
res = res.substring(0, res.length() - 3);
return res;
}
}
Dans la méthode attaquer(), on est obligé de faire une boucle for pour parcourir tous les ennemis de la bande. En général, on a deux possibilités pour faire cette boucle :
La boucle for traditionnelle : for (int i=0; i < ennemis.getSize(); i++)
La boucle range-for ou for-each : for (var ennemi : ennemis)
La seconde ne s’applique pas tout le temps mais elle a le mérite de cacher la structure interne du MyArray. Cette seconde boucle utilise des itérateurs.
Définition
L’objectif du design pattern Iterator est de fournir des classes (itérateurs) qui permettent de parcourir les éléments d’une collection sans révéler la représentation interne de cette dernière.
L’idée consiste à faire en sorte que la collection d’objets (MyArray pour notre jeu de rôle) implémente une interface Iterable dont l’objectif est d’imposer l’existence d’une méthode créant des itérateurs. Ici, la collection d’objets crée ainsi des instances de ConcreteIterator. Ceux-ci implémentent les méthodes communes à tous les itérateurs : une méthode hasNext() pour indiquer s’il reste encore des éléments à parcourir dans la collection et une méthode next() qui permet de passer à l’élément suivant dans la collection.
En ce qui concerne MyArray, un code possible est le suivant :
// contient les interfaces Iterable et Iterator
import java.util.Iterator;
public class MyArray<T> implements Iterable<T> {
private T[] array;
private int size = 0;
// on définit ici ce qu'est un itérateur
public class MyIterator<T> implements Iterator<T> {
private MyArray<T> myArray; // agrégation
private int index = 0; // l'itérateur pointe sur le 1er élément
public MyIterator(MyArray<T> array) {
this.myArray = array;
}
@Override
public boolean hasNext() {
return (this.index < this.myArray.size);
}
@Override
public T next() {
if (this.index < this.myArray.size)
return this.myArray.array[this.index++];
else
return null;
}
}
// les méthodes de la classe MyArray
public MyArray(int capacity) {
if (capacity <= 1) capacity = 2;
this.array = (T[]) new Object[capacity];
}
public Iterator<T> iterator() {
return new MyIterator<T>(this);
}
}
Ici, MyArray<T> implémente l’interface classique de Java Iterable<T>. Cela signifie que MyArray<T> s’engage à fournir une méthode iterator() qui renverra un itérateur pointant sur son premier élément. La définition de cet itérateur (classe MyIterator<T>) est réalisée à l’intérieur de la classe MyArray<T> (au tout début de la classe). Cela a son importance car cela permettra à MyIterator<T> d’accéder aux champs privés de MyArray<T>, ce qui simplifie le code de cet itérateur. Initialement, quand on crée l’itérateur, ce dernier pointe sur le premier élément de l’attribut array de MyArray<T>. Chaque fois qu’on a besoin d’un élément, on appelle la méthode next() qui nous le renvoie. La méthode hasNext(), elle, renvoie un booléen indiquant si la méthode next() est en capacité de nous renvoyer un élément. Dit autrement, l’équivalent de la boucle for (int i=0; i < ennemis.getSize(); i++) {…} est :
var iter = ennemis.iterator();
while (iter.hasNext()) {
var ennemi = iter.next();
.....
}
Avantages :
Les itérateurs permettent de respecter le principe Ouvert/Fermé : quand on remplace une collection (MyArray) par une autre (ArrayList), on n’a rien à modifier dans notre code. Les appels aux itérateurs pour parcourir ces collections sont les mêmes.
Le principe de responsabilité unique est également préservé puisque les parcours des collections sont entièrement réalisés par les itérateurs et c’est leur unique tâche.
Les itérateurs offrent des possibilités de parcours assez flexibles. Par exemple, on peut imaginer dans un arbre binaire de recherche des itérateurs pour réaliser des parcours en profondeur, d’autres en largeur, etc.
Inconvénients :
Je ne vois pas trop d’inconvénient à utiliser des itérateurs.
Utilisez le patron iterator pour parcourir les scanners de proximité de l’USS Orville afin de déterminer si un objet dangereux est à proximité du vaisseau.