Dans ce chapitre, nous découvrirons les avantages et les applications liés à l'utilisation de design patterns de création.
Pourquoi utiliser des patterns de création ?
Pour que vos systèmes fonctionnent, vous avez besoin d'instancier des objets. En général, vous créez ceux dont vous avez besoin, au moment voulu. Vous verrez souvent le code suivant :
new someClassName();
Mais ce code pose un problème. Oui, oui : vous n'avez aucune flexibilité ! Vous créez un objet de type someClassName. Pour toujours. À moins de revenir en arrière sur votre code. Comme déjà vu précédemment, le retour en arrière et la modification de code existant peuvent entraîner des problèmes. Alors, y a-t-il une autre solution ? Essayez autant que possible d'éviter d'utiliser le mot-clé « new ».
Mais, c’est avec new que je crée de nouveaux objets ! Comment l'éviter ?
Vous ne pouvez pas l'éviter totalement, mais vous pouvez l'utiliser de manière plus judicieuse. Il existe un adage, en développement logiciel, qui affirme que tous les problèmes de programmation peuvent être résolus grâce à un niveau d’abstraction supplémentaire. Si vous l'appliquez, vous pouvez confier la création de votre objet à “quelqu’un d’autre”. Ce quelqu’un d’autre est certes du code nouveau, mais vous gagnez en souplesse dans votre code et ce, via un niveau supplémentaire d’abstraction.
Et c'est à ce moment que les design patterns de création peuvent vous venir en aide ! Examinons quelques exemples spécifiques.
Qu'est-ce que le pattern Factory ?
Le pattern Factory (fabrique) consiste à confier à d'autres objets le soin de créer des objets pour vous. L'objet Factory est configuré d'une certaine manière, puis invité à créer l'objet voulu.
Certains jeux ne nécessitent que 32 cartes pour jouer. Dans quelle mesure pourrions-nous modifier notre application de manière à utiliser seulement 32 cartes ? Le paquet de cartes est généré dans le contrôleur, ou il lui est transmis. Nous allons donc transférer la création du jeu dans une classe Factory. Une Factory est une classe qui instancie des objets d'une autre classe. Si vous modifiez la Factory, vous modifiez ce qui est créé. Nous pouvons donc avoir des Factory de 32 et 52 cartes. Nous choisissons celle correspondant au type de jeu que nous utilisons. Elle crée le paquet de cartes, et nous le transmettons au contrôleur.
Voici une classe DeckFactory qui crée des jeux normaux, de petite taille et de test, en fonction du paramètre :
public class DeckFactory {
public static Deck makeDeck(DeckType type) {
switch (type) {
case Normal: return new NormalDeck();
case Small: return new SmallDeck();
case Test: return new TestDeck();
}
// fallback
return new NormalDeck();
}
}
Ensuite, toute classe ayant besoin d'un jeu normal pourrait effectuer l'appel suivant :
Deck myDeck = DeckFactory.makeDeck(DeckType.Normal);
Effectuons cela ensemble :
Nous avons ajouté une certaine flexibilité en demandant à un autre objet (DeckFactory) de créer l'objet que nous voulons dans notre jeu (NormalDeck, SmallDeck ou TestDeck). Les patterns peuvent être modifiés en fonction de nos besoins. Nous pourrions ajouter une méthode à DeckFactory pour préconfigurer le type de jeux créés par ce dernier. Dans ce cas, la nouvelle méthodemakeDeck()
n'aurait pas besoin de paramètre et renverrait le type de jeu précédemment configuré. C'est pratique lorsque vous écrivez des tests unitaires. Durant le test, le type de jeu serait paramétré surTest
. Et pour un vrai jeu, il serait paramétré sur Normal
ou Small
.
L'approche Factory est utile quand nous souhaitons pouvoir choisir parmi plusieurs implémentations spécifiques, au moment de l'exécution. Il vous suffit d'indiquer à la Factory votre demande, et elle y répondra !
En quoi consiste le pattern Prototype ?
Les patterns de création Prototype créent un nouvel objet en clonant un objet déjà existant. De nouveau, vous préconfigurez un ou plusieurs objets, des "modèles". Ensuite, vous sélectionnez celui auquel doit ressembler votre nouvel objet, et vous demandez au prototype vous retournez un clone du modèle.
C'est comme cela que les cellules de votre corps se renouvellent ! Lorsqu'une cellule se divise, elle crée une copie exacte d'elle-même.
OK, mais comment ça fonctionne en Java ?
En Java, une affectation se fait par référence. Par exemple, dans le code ci-dessous, a et b font tous deux référence au même objet.
SomeClass a = new SomeClass();
SomeClass b = a;
// this also changes a.someField
// since a and b refer to the same thing
b.someField = 5;
Si vous souhaitiez un objet totalement nouveau pour b
, mais encapsulant les mêmes valeurs que a
au moment de la copie, vous utiliseriez un clone :
SomeClass a = new SomeClass();
a.someField = 1202;
SomeClass b = a.clone();
// a.someField remains 1202
// since it is a different object
b.someField = 5;
Et voilà le résultat !
Le clonage est un pattern pratique lorsque vous voulez une copie exacte, ou du moins très proche, d'un objet existant. Il est également possible de créer un objet de façon normale (appel de new), puis de setter toutes les valeurs à l'identique de l'objet modèle. Mais le code sera plus lourd à écrire...
Une autre mise en œuvre consiste à faire une recherche dans un catalogue. Vous créez un ensemble d'objets préconfigurés que vous enregistrez dans un catalogue, avec une clé de recherche. Ensuite, quand vous avez besoin d'un objet d'un type particulier, vous demandez au catalogue d'en fournir un. Le catalogue trouve l'objet correspondant à la clé, le clone, et vous donne le nouvel objet à utiliser.
En quoi consiste le pattern Builder ?
Il arrive souvent de créer un objet à partir de sous-éléments constitutifs, qui doivent être assemblés dans un certain ordre. Mais comment contrôler la cohérence de l'ensemble ? Grâce au pattern Builder ! Le builder suit un algorithme afin de réaliser l’assemblage des sous-parties. Et ces sous-parties peuvent varier.
Si vous avez déjà commandé une formule dans un restaurant, vous avez fait l'expérience du pattern Builder. Chaque formule se compose d’un plat, d'un dessert et d'une boisson. Mais les plats peuvent changer. Pour le plat, vous avez le choix entre une viande, un poisson ou une omelette. Le dessert peut être une tarte, une crème ou un fruit. La boisson peut être un apéritif, un verre de vin ou un café. La personne qui prend votre commande ajoute un élément de chaque catégorie pour constituer la formule, et le résultat se compose toujours de trois éléments.
Dans notre jeu de cartes, nous pourrions utiliser ce pattern (même s'il manque un peu de maniabilité). Nous disposons d'un ensemble d'options à sélectionner. Nous pouvons sélectionner la taille du jeu, ainsi que les options de carte forte ou faible pour le gagnant. Notre objet Builder renverrait les implémentations adéquates, en fonction des options choisies.
public interface GameBuilder {
Game getGame();
}
public class NormalHighCardGameBuilder implements GameBuilder {
public Game getGame() {
return new Game(DeckFactory.makeDeck(DeckType.Normal), EvaluatorFactory.makeEvaluator(EvaluatorType.High));
}
}
public class SmallHighCardGameBuilder implements GameBuilder {
public Game getGame() {
return new Game(DeckFactory.makeDeck(DeckType.Small),EvaluatorFactory.makeEvaluator(EvaluatorType.High));
}
}
En quoi consistent les singletons ?
Nous l’avons déjà vu dans le chapitre sur les pratiques STUPID, les singletons sont un risque. Relisez cette section si vous avez besoin de vous rafraîchir la mémoire. L'un des auteurs du livre sur les design patterns a déclaré que s'il pouvait réécrire ce livre, il aurait laissé celui-ci de côté.
Si nous avions créé un SoundManager et choisi d'en faire un singleton, le résultat aurait été le suivant :
class SoundManager
{
// il faut garder en mémoire l'instance unique quelque part
// nous allons donc garder une variable statique
private static SoundManager _instance = null;
// comme n'importe quelle classe, il y a des attributs
private int currentSoundLevel;
// rendre le constructeur privé
// afin qu'il ne soit appelé par rien d'autre
// que les méthodes de cette classe même
private SoundManager() {
currentSoundLevel = 11;
}
// la méthode que les clients appellent pour accéder au singleton
public static SoundManager getInstance() {
// si nous n'avons pas déjà d'instance, en créer une maintenant
if (_instance == null)
_instance = new SoundManager();
// retourner celle que nous venons de faire
// ou que nous avions déjà faite
return _instance;
}
// rien de spécial à propos des autres méthodes
public void setVolume(int value) {
currentSoundLevel = value;
}
}
L'essentiel est que le constructeur soit rendu privé. Mais nous devons toujours fournir une méthode pour accéder au SoundManager que nous créons. C'est le rôle de la méthode getInstance()
.
En résumé
Les patterns de création ajoutent un niveau d’abstraction à la création d'objets, ce qui offre plus de flexibilité aux applications.
Le pattern Factory consiste à créer un objet pour un autre objet. Vous pouvez configurer votre Factory pour créer un objet comme vous le souhaitez.
Le pattern Prototype permet de créer un nouvel objet en clonant un objet existant.
Le pattern Builder permet de créer un objet complexe en assemblant divers autres objets et en suivant un algorithme.
Maintenant que vous savez comment créer des objets de différentes manières, examinons comment il est possible d'organiser des objets en structures complexes. Dans le prochain chapitre, nous allons découvrir quelques design patterns de structure.