Design Pattern – TD n°3 : Builder et Singleton
Pattern Builder (Jeux Vidéo)
Vous êtes intégré dans l'équipe de développement d'un nouveau jeu vidéo, plus précisément dans la gestion des armes.
1. Le code existant gère trois types d'armes : pistolet, tronçonneuse et tricycle. Pour créer un objet de type Gun, il faut l'instancier avec un new, puis exécuter une liste d'ordres définie dans le main. Cette liste dépend de l'architecture du jeu et peut varier. Si un ordre non prévu est demandé, il est ignoré.
L'objectif est de faire charger correctement un objet de type Saw en exécutant tous les ordres programmés.
2. La semaine précédente, le pattern Factory a été abordé. Implémentez ce pattern pour simplifier la création et l'exécution des ordres, en rendant le processus transparent pour le client. Une interface Weapon devra être créée.
3. L'architecture actuelle présente un défaut : certaines fonctions sont mal placées. Par exemple, dans Gun, tout ce qui n'est pas lié au constructeur devrait être déplacé vers une classe dédiée au préchargement, nommée GunBuilder.
Créez GunBuilder avec : - Une variable interne de type Gun - Une méthode CreateNewWeapon - Une méthode GetWeapon - Toutes les fonctions de Gun (redéfinies dans GunBuilder).
Créez également SawBuilder et BikeBuilder.
4. Développez une classe Director qui orchestrera la construction des armes via leurs builders respectifs : - Gun avec GunBuilder - Saw avec SawBuilder - Bike avec BikeBuilder
5. Simplifiez le Director en définissant une classe abstraite AbstractWeaponBuilder qui implémente toutes les fonctions des builders (avec une implémentation vide par défaut). Chaque builder spécialisé (GunBuilder, SawBuilder, BikeBuilder) héritera de cette classe et surchargera uniquement les méthodes nécessitant un comportement spécifique.
6. Optimisez le code du Director pour qu'il n'ait plus de logique spécifique à chaque arme. L'architecture finale devrait refléter le pattern Builder : - Délégation de la construction complexe aux sous-classes spécialisées - Utilisation d'un Director pour l'assemblage.
7. Ajoutez une nouvelle arme. Le code existant doit-il être modifié ? Si cette arme nécessite une nouvelle fonction de préchargement (par exemple, Preload3dEffects), l'ajout reste-t-il simple ?
Pattern Singleton (Aéroport)
On souhaite créer une classe Aeroport et des objets Avion.
Voici le code initial des classes Aeroport et Avion ainsi que de leur test :
public class Aeroport {
public Aeroport() {
piste_libre = true;
}
}
class Avion extends Thread {
String nom;
Aeroport a;
public Avion(String s) {
nom = s;
}
public void run() {
a = new Aeroport();
System.out.println("Je suis avion " + nom + " sur aeroport " + a);
}
}
class testaeroport {
public static void main(String[] args) {
Avion v1 = new Avion("Avion 1");
Avion v2 = new Avion("Avion 2");
Avion v3 = new Avion("Avion 3");
Avion v4 = new Avion("Avion 4");
v1.start();
v2.start();
v3.start();
v4.start();
}
}
2. La méthode start sur les objets Avion déclenche l'exécution du thread, qui instancie un nouvel Aeroport via la méthode run. Chaque avion crée ainsi son propre aéroport.
3. Implémentez le pattern Singleton pour empêcher la création de plusieurs instances d'Aeroport : - Une variable de classe statique de type Aeroport - Un constructeur privé - Une méthode statique getInstance qui crée un Aeroport s'il n'existe pas, et renvoie l'instance existante sinon.
4. Pour tester la robustesse multi-thread, ajoutez un délai de 500ms avant la création de l'Aeroport dans la méthode run :
try {
Thread.sleep(500);
} catch(Exception e) {}
Testez le code : plusieurs aéroports peuvent être créés simultanément par différents threads.
5. Utilisez le mot-clé synchronized pour garantir qu'un seul thread crée l'Aeroport à la fois. Voici une solution possible :
public class Aeroport {
private static Aeroport instance;
private static boolean initialized = false;
private Aeroport() {
piste_libre = true;
}
public static synchronized Aeroport getInstance() {
if (!initialized) {
try {
Thread.sleep(500);
} catch(Exception e) {}
instance = new Aeroport();
initialized = true;
}
return instance;
}
}
Il existe des alternatives plus performantes, comme le double-checked locking.
Labyrinthe et Patterns (Dons & Dragons)
Un labyrinthe doit être créé où des joueurs (gérés par des threads se déplaçant aléatoirement) évoluent.
1. Le Labyrinthe ne doit pouvoir être instancié qu'une seule fois. Il contient une matrice de Salles représentant le labyrinthe. Les salles peuvent être : - SalleAuTresor - SalleAvecMonstre - SalleAvecPiege - SalleVide - Une seule SalleSortie.
La position de la SalleSortie est déterminée dès la création.
2. Chaque Salle possède des Sorties (Est, Ouest, Nord, Sud) qui peuvent être activées ou non. Lors de la création d'une salle, un objet Sortie doit être généré, avec la méthode hasSortie(direction) pour vérifier l'existence d'une sortie dans une direction donnée.
Cette architecture doit permettre d'ajouter ultérieurement des sorties diagonales (Nord-Est, Nord-Ouest, etc.).
3. Lors de la génération des salles, assurez-vous que chaque joueur puisse atteindre la SalleSortie depuis sa zone du labyrinthe, sans risque de s'y perdre définitivement.
4. Les salles suivantes contiennent des éléments spécifiques : - SalleAvecMonstre : un Monstre - SalleAvecPiege : un Piège - SalleAuTresor : un Trésor.
Ces éléments peuvent être de différents types.
5. Les joueurs ont un niveau qui augmente avec leur progression. Ils doivent : - Battre un monstre plus fort (combat type Pierre-Feuille-Ciseaux avec handicap) - Rebrousser chemin s'ils perdent - Soudoyer le monstre avec leur or - Éviter un piège en tirant un nombre aléatoire (sinon, paralysie ou déplacement dans le labyrinthe).
FAQ
1. Pourquoi utiliser le pattern Builder ?
Le pattern Builder simplifie la création d'objets complexes en séparant la construction de leur représentation. Cela évite de modifier le code existant lors de l'ajout de nouvelles armes.
2. Quels sont les risques d'une mauvaise implémentation du Singleton en multi-thread ?
Sans synchronisation, plusieurs threads peuvent créer des instances distinctes du Singleton, violant ainsi son principe. Utiliser synchronized ou des techniques comme le double-checked locking garantit une seule instance.
3. Comment s'assurer qu'un joueur ne reste pas bloqué dans le labyrinthe ?
Lors de la génération des salles, vérifiez que chaque chemin mène éventuellement à la SalleSortie. Utilisez des algorithmes comme la recherche en largeur (BFS) pour garantir la connectivité.