Design pattern Fabrique (Factory Method) en Java : jeu de stratégie
Rédigé par Mathieu G. - Ecrit le 17/11/2011 et mis à jour le 31/12/2022
Implémentation en Java du design pattern Fabrique lors du développement d'un jeu de stratégie.
Description du problème
L'idée est de développer les bases d'un jeu de stratégie en temps réel (RTS pour les gamers). Dans ce type de jeu, la plupart des unités permettent de faire la guerre aux autres joueurs. Le vainqueur sera le dernier joueur disposant encore d'unités sur la carte. Généralement, les unités sont construites dans des bâtiments (usines, casernes...) grâce à des ressources (minerais, bois...) qu'il faut prélever sur la carte.
Pour cela, nous allons définir une classe abstraite Unite disposant d'un nom, d'un coût de construction, d'une précision d'attaque, d'une faculté d'esquive...
Implémentation de la classe abstraite Unite// Classe abstraite dont toutes les unités du jeu hériteront.
public abstract class Unite
{
protected String nom;// Nom de l'unité.
protected int coutConstruction;// Coût de construction de l'unité.
protected int precisionAttaque;// Précision de l'attaque de l'unité.
protected int esquiveDefense;// Faculté d'esquiver une attaque de l'unité.
protected ArrayList equipements;// Tableau des équipements de l'unité.
// Méthode qui consomme les ressources pour créer une unité.
public void consommerRessource()
{
System.out.println("Consomme "+this.coutConstruction+" ressources pour la création de l'unité.");
}
// Méthode abstraite qui permet d'équiper l'unité.
public abstract void equiper();
// Méthode générique pour l'affichage de l'unité.
public String toString()
{
String str = "Nom : "+this.nom+"\n";
str += "Coût de construction : "+this.coutConstruction+"\n";
str += "Précision d'attaque : "+this.precisionAttaque+"\n";
str += "Esquive en défense : "+this.esquiveDefense+"\n";
str += "Equipements : ";
for(int i=0; i<this.equipements.size(); i++)
{
str += this.equipements.get(i)+" ";
}
return str;
}
}
Toutes les unités de notre jeu vont hériter de cette classe comme par exemple la classe SoldatHumain et CommandantHumain en ayant chacune leurs propres spécificités (le commandant est plus coûteux que le soldat mais dispose d'une précision d'attaque supérieure).
Implémentation des classes SoldatHumain et CommandantHumain// Classe représentant un soldat humain.
public class SoldatHumain extends Unite
{
// Constructeur pour un soldat humain.
public SoldatHumain()
{
this.nom = "Fantassin";
this.coutConstruction = 5;
this.precisionAttaque = 1;
this.esquiveDefense = 2;
this.equipements = new ArrayList();
}
// Equiper un soldat humain.
public void equiper()
{
this.equipements.add("Pistoler");
this.equipements.add("Bouclier");
System.out.println("Equipement d'un soldat humain (Pistoler, Bouclier).");
}
}
// Classe représentant un commandant humain.
public class CommandantHumain extends Unite
{
// Constructeur pour un commandant humain.
public CommandantHumain()
{
this.nom = "Lieutenant";
this.coutConstruction = 14;
this.precisionAttaque = 5;
this.esquiveDefense = 2;
this.equipements = new ArrayList();
}
// Equiper un commandant humain.
public void equiper()
{
this.equipements.add("Uzi");
this.equipements.add("Bouclier");
System.out.println("Equipement d'un commandant humain (Uzi, Bouclier).");
}
}
Une classe Usine va permettre de créer ces unités grâce à la méthode formerUnite(TypeUnite). La première idée qui vient à l'esprit pour implémenter cette méthode est de tester le type d'unité souhaité passé en paramètre (via une succession de if ou mieux un switch) et de créer un objet correspondant à l'unité demandée (par exemple : new SoldatHumain()).
Ce code va fonctionner mais on remarque qu'en utilisant l'opérateur new, on vient de déclarer des types concrets dans la classe Usine ce qui a pour conséquence de fortement coupler la classe Usine aux classes SoldatHumain et CommandantHumain. Est-ce un mal, car il faudra bien créer des objets concrets et l'opérateur new est conçu pour cela ? En effet, il n'est pas envisageable de développer un programme orienté objet en Java sans utiliser l'opérateur new et donc d'utiliser des types concrets. Cependant dans ce cas précis, on peut facilement prévoir que cette portion du code va être amené à évoluer lors de l'ajout de nouvelles unités. De plus, si plusieurs bâtiments différents permettent de créer des unités, les modifications devront être reporté dans plusieurs endroits du jeu. Cela va donc vite devenir de plus en plus fastidieux d'ajouter des unités et des bâtiments et surtout source d'erreurs en étant obligé de modifier le code existant.
Implémentation d'un début de solution avec une Simple Fabrique
Pour répondre à cette problématique on va implémenter une simple fabrique, qui pour rappel est une bonne pratique de conception mais pas un design pattern a proprement parlé. Le principe est de regrouper l'instanciation des unités dans une seule classe, ici SimpleFabrique, afin d'éviter la duplication de cette portion du code vouée aux évolutions.
Implémentation de la classe SimpleFabrique// Classe permettant de fabriquer une unité.
public class SimpleFabrique
{
// La création d'une unité en fonction de son type est encapsulée dans la fabrique.
public Unite creerUnite(TypeUnite type)
{
Unite unite = null;;
switch(type)
{
case SOLDAT:unite = new SoldatHumain();break;
case COMMANDANT:unite = new CommandantHumain();break;
}
return unite;
}
}
// Enumération des types d'unités.
public enum TypeUnite
{
SOLDAT,
COMMANDANT
}
La classe SimpleFabrique a donc pour seule responsabilité d'instancier l'unité correspondant au type passé en paramètre de sa méthode creerUnite(TypeUnite). Au passage on peut noter l'utilisation d'une énumération en java pour ce paramètre qui est bien plus sûr qu'une simple chaîne de caractères (et surtout qui est utilisable avec l'instruction switch).
Implémentation de la classe Usine// Classe usine qui représente un bâtiment capable de construire des unités.
public class Usine
{
private SimpleFabrique simpleFabrique;// Attribut contenant la fabrique simple.
// Le constructeur permet de sélectionner la fabrique à utiliser.
public Usine()
{
this.simpleFabrique = new SimpleFabrique();
}
// Méthode qui permet de construire l'ensemble des unités.
public Unite formerUnite(TypeUnite type)
{
Unite unite = this.simpleFabrique.creerUnite(type);
unite.consommerRessource();
unite.equiper();
return unite;
}
}
La classe Usine conserve un objet SimpleFabrique dans un attribut initialisé dans son constructeur. La méthode formerUnite(TypeUnite) n'instancie pas directement des unités mais délègue cette tâche à l'objet SimpleFabrique. On remarque alors que la classe Usine est maintenant couplée fortement à la seule classe SimpleFabrique via une relation de composition.
Si une nouvelle classe correspondant à un autre bâtiment est ajoutée il suffira d'utiliser la simple fabrique pour instancier les unités. Quand à l'ajout d'unités, il ne nécessite maintenant que la modification de la seule classe SimpleFabrique.
Implémentation de la classe Main// Classe principale du projet.
public class Main
{
// Méthode principale.
public static void main(String[] args)
{
Usine usine = new Usine();
Unite unite = usine.formerUnite(TypeUnite.SOLDAT);
System.out.println(unite);
}
}
La classe Main correspond à la classe principale de notre jeu. Elle va nous permettre de créer une usine afin d'y former un soldat.
Résultat en consoleConsomme 5 ressources pour la création de l'unité.
Equipement d'un soldat humain (Pistoler, Bouclier).
Nom : Fantassin
Coût de construction : 5
Précision d'attaque : 1
Esquive en défense : 2
Equipements : Pistoler Bouclier
Pour notre grand bonheur le jeu fonctionne et le résultat correspond à nos attentes.
Cette bonne pratique, nous a donc permis de répondre de façon élégante à la problématique rencontrée. Mais notre jeu va devoir se complexifier car les retours des bêta-testeurs indiquent que celui-ci manque de diversité. Il serait intéressant de créer plusieurs races ayant chacune des unités (Soldat, Commandant...) et des bâtiments. Idéalement il faudrait que des développeurs externes au projet puissent proposer de nouvelles races sous forme d'addons sans toucher aux fichiers de l'application (afin de maîtriser la qualité du jeu et de rendre possible l'utilisation de plusieurs addons simultanément).
Ci-dessous, le diagramme de classe correspondant à ces nouveaux besoins et qui utilise le design pattern Fabrique afin de répondre aux contraintes évoquées.
Implémentation avec le design pattern Fabrique
Aucun changement concernant les classes Unite, SoldatHumain et CommandantHumain. Cependant, on note l'ajout de deux nouvelles classes correspondant aux soldats et aux commandants extraterrestres.
Implémentation des classes SoldatExtraterrestre et CommandantExtraterrestre// Classe représentant un soldat extraterrestre.
public class SoldatExtraterrestre extends Unite
{
// Constructeur pour un soldat extraterrestre.
public SoldatExtraterrestre()
{
this.nom = "Alien";
this.coutConstruction = 6;
this.precisionAttaque = 2;
this.esquiveDefense = 1;
this.equipements = new ArrayList();
}
// Equiper un soldat extraterrestre.
public void equiper()
{
this.equipements.add("Acide");
this.equipements.add("Peau");
System.out.println("Equipement d'un soldat extraterrestre (Acide, Peau).");
}
}
// Classe représentant un commandant extraterrestre.
public class CommandantExtraterrestre extends Unite
{
// Constructeur pour un commandant extraterrestre.
public CommandantExtraterrestre()
{
this.nom = "Prédateur";
this.coutConstruction = 10;
this.precisionAttaque = 3;
this.esquiveDefense = 3;
this.equipements = new ArrayList();
}
// Equiper un commandant extraterrestre.
public void equiper()
{
this.equipements.add("Mitraillette à plasma");
this.equipements.add("Peau");
System.out.println("Equipement d'un commandant extraterrestre (Mitraillette à plasma, Peau).");
}
}
L'implémentation de ces classes n'offre aucune surprise par rapport aux classes SoldatHumain et CommandantHumain. Seules les caractéristiques des unités (coût de construction, précision d'attaque...) changent. Les modifications les plus intéressantes se situent dans les classes représentant les usines.
Implémentation de la classe abstraite Usine// Usine abstraite qui sert de base aux usines concrètes.
public abstract class Usine
{
// Méthode qui permet de former les unités.
public Unite formerUnite(TypeUnite type)
{
Unite unite = this.creerUnite(type);
unite.consommerRessource();
unite.equiper();
return unite;
}
// La création d'une unité est déléguée aux sous classes.
public abstract Unite creerUnite(TypeUnite type);
}
La classe Usine est maintenant abstraite, car elle déclare une méthode creerUnite(TypeUnite) abstraite. On conserve la méthode formerUnite(TypeUnite) mais celle-ci fait maintenant appel à sa méthode creerUnite(TypeUnite) déclarée abstraite pour instancier les unités.
La classe Usine étant maintenant abstraite, elle ne permet plus directement de créer et former des unités, il va falloir pour cela créer des sous-classes. L'objectif de l'implémentation de la classe Usine étant de déléguer l'instanciation des unités à ses sous-classes, ce qui correspond exactement à la description du design pattern Fabrique. A noter que la classe Usine conserve toute de même la responsabilité de la formation des unités pour assurer un jeu cohérent (pas question de créer des unités sans consommer des ressources ou sans les équiper). Pour plus de sécurité, on aurait pu déclarer la méthode formerUnite(TypeUnite) comme « final » afin que ses sous classes ne puissent pas la redéfinir.
Implémentation des classes UsineHumain et UsineExtraterrestre// Usine humaine.
public class UsineHumain extends Usine
{
// Méthode qui permet de créer des unités humaines.
public Unite creerUnite(TypeUnite type)
{
Unite unite = null;;
switch(type)
{
case SOLDAT:unite = new SoldatHumain();break;
case COMMANDANT:unite = new CommandantHumain();break;
}
return unite;
}
}
// Usine extraterrestre.
public class UsineExtraterrestre extends Usine
{
// Méthode qui permet de créer des unités extraterrestres.
public Unite creerUnite(TypeUnite type)
{
Unite unite = null;;
switch(type)
{
case SOLDAT:unite = new SoldatExtraterrestre();break;
case COMMANDANT:unite = new CommandantExtraterrestre();break;
}
return unite;
}
}
Les classes UsineHumain et UsineExtraterrestre héritent simplement de la classe Usine et implémentent la méthode creerUnite(TypeUnite) permettant l'instanciation des unités du jeu.
Implémentation de la classe Main// Classe principale du projet.
public class Main
{
// Méthode principale.
public static void main(String[] args)
{
Usine usineExtraterrestre = new UsineExtraterrestre();
Unite unite = usineExtraterrestre.formerUnite(TypeUnite.SOLDAT);
System.out.println(unite);
}
}
On modifie la précédente classe Main afin de tester la création d'un soldat extraterrestre grâce à la nouvelle usine d'extraterrestres.
Résultat en consoleConsomme 6 ressources pour la création de l'unité.
Equipement d'un soldat extraterrestre (Acide, Peau).
Nom : Alien
Coût de construction : 6
Précision d'attaque : 2
Esquive en défense : 1
Equipements : Acide Peau
Le résultat obtenu en console est une fois de plus à la hauteur de nos attentes. On obtient bien un soldat extraterrestre après avoir consommé les ressources nécessaires à sa construction puis l'avoir équipé.
Conclusion
Grâce au design pattern Fabrique l'ajout d'une nouvelle race dans notre jeu ne nécessite pas de modifier le code existant (exceptée la classe principale Main). En effet, il suffit de créer les classes correspondantes aux unités (qui hériteront de la classe Unite) puis de créer l'usine correspondante (voir même les usines). Seul l'ajout d'une unité nécessite de modifier les classes usines concrètes permettant de les instancier. Mais cela semble tout à fait logique car si aucune usine ne permet de créer cette unité il ne sera pas possible de la rencontrer dans le jeu.
La pattern Fabrique répond à la problématique courante des développeurs d'encapsuler la création des objets. Les principales raisons sont de découpler une partie du code des classes concrètes que l'on doit instancier ou de permettre facilement d'ajouter des classes concrètes ultérieurement. A noter que si votre besoin correspond à la création d'une famille de produits, il existe le design pattern Fabrique Abstraite (son cousin) qui y répond parfaitement.
Commentaires
design pattern Fabrique
Soumis par cedric s le Dimanche 04/12/2011 04:58
Salut Mathieu,
Après lecture de ces exemples, je comprend encore mieux ce pattern. Cependant il y a quelques erreurs de frappe (je pense):
Dans la méthode toString de la classe Unité tu as oublié de fermer la boucle for, de retourner le string que tu construis (return str) et enfin de fermer les accolades.Déjà à quoi sert cette boucle for?
Aussi j'ai une suggestion: dans le cas d'une fabrique simple pourquoi pas placer la méthode creerUnite en static.?
je trouve que tu métrises très bien le JAVA pour un gars qui ne fait que du PHP.
Merci.
design pattern Fabrique
Soumis par Mathieu G. le Mercredi 07/12/2011 22:18
Salut Cédric,
Merci pour ta remarque concernant la méthode toString() de la classe Unite.
Je viens de corriger cela dans l'article (sans doute un mauvais copier/coller).
Tu pourras donc voir que cette boucle for sert à afficher la liste des équipements d'une unité.
Très bonne remarque, il est possible de définir la méthode creeUnite(TypeUnite) comme statique afin d'éviter de devoir instancier un objet de la classe SimpleFabrique (c'est d'ailleurs une pratique que l'on rencontre réguliérement) mais il est préférable d'éviter.
En effet, cette technique a un inconvénient : il n'est plus possible de sous classer la méthode creeUnite(TypeUnite) si on a besoin de la faire évoluer ultérieurement.
A bientôt.
Erreur ??
Soumis par Laurent le Mardi 24/01/2012 12:51
Bonjour et bravo pour cet article très bien détaillée.
Juste une petite remarque : Sur le diagramme UML du chapitre "Implémentation avec le design pattern Fabrique", la classe abstraite "Usine" ne devrait plus posséder l'attribut de type SimpleFabrique.
Super
Soumis par NargiT le Jeudi 26/01/2012 15:25
Magnifique!
Super exemple ;)
Erreur ??
Soumis par Mathieu G. le Samedi 28/01/2012 15:17
Merci Laurent pour ta remarque je viens de corriger le diagramme UML.
Merci également à NargiT, j'espère faire aussi bien pour les prochains ;)
Super tuto
Soumis par Doude le Jeudi 01/03/2012 11:15
On en veut encore !! D'autres tuto SVP !!!!
Super tuto
Soumis par Mathieu G. le Dimanche 11/03/2012 17:31
Ca arrive, il y a un nouvel article en préparation.
Je sais que je suis un peu long en ce moment mais il ne faut pas désespérer ;)
Bravo ! Mais il reste une faute de frappe ^^
Soumis par TLeM4 le Mercredi 16/10/2013 16:37
protected ArrayListequipements;// Tableau des équipements de l'unité.
Doit surement manquer d'un espace :)
Autrement cet article m'a vraiment permis de comprendre le pattern frabrique.
Bravo !
Bravo ! Mais il reste une faute de frappe ^^
Soumis par Mathieu G. le Lundi 21/10/2013 23:18
C'est corrigé.
Merci d'avoir pris le temps de signaler cette erreur pour les futurs lecteurs :)
Très bon tuto
Soumis par Hadrien le Samedi 18/01/2014 23:01
Merci pour ce tuto, qui je trouve est un bel exemple concret avec la pointe de théorie qu'il faut.
C'est une très bonne explication du Pattern Factory !
Je pense que je vais aller me délecter d'autres tutos du site.
Merci encore et bon courage.
Très bon tuto
Soumis par Mathieu G. le Lundi 20/01/2014 22:05
Merci Hadrien c'est motivant.
Bonne lecture pour les autres tutos :)
Très clair mais il me reste une question
Soumis par Sierramike le Jeudi 20/03/2014 10:34
Bonjour,
Ton tuto est très clair et je t'en remercie ! Néanmoins, il me reste une problématique, peut-être connais-tu un design pattern qui y réponde :
J'ai mon petit framework personnel, avec une usine qui crée des soldats et des commandants.
Dans une application séparée qui utilise mon framework, j'ai tout à coup besoin, en complément des deux autres, de colonels.
Je vais donc créer mon colonel, dérivant d'unité.
Mais ensuite ?
Je pourrais créer mon UsineLocale, héritant d'UsineHumaine, qui teste si TypeUnite est COLONEL, elle me fabrique un colonel, sinon elle demande à UsineHumaine (base) de faire le job. Est-ce une bonne pratique ?
Mais là où ça se complique encore : quel est ton implémentation de TypeUnite ? Si c'est une enum, peut-on rajouter un élément à une énum à travers l'héritage ? Là, je me retrouve confronté à un vrai soucis ...
Merci de ton aide !
Très clair mais il me reste une question
Soumis par Mathieu G. le Dimanche 23/03/2014 22:08
Bonjour Sierramike,
Si cela est cohérent du point de vue fonctionnel, je ne vois pas d'inconvénient à ce que UsineLocale hérite de UsineHumaine.
Attention de s'assurer qu'il est logique que toutes les unités crées par UsineHumaine soient également crées par UsineLocale (y compris à l'avenir).
Dans mon exemple TypeUnite est effectivement une énumération.
Mais cette liste peut être construite à partir d'autres sources de données comme une base de données, un fichier XML...
Si nécessaire, n'hésite pas à me contacter par mail en détaillant ton besoin.
Encore !!!!
Soumis par Alexandre L. le Mercredi 09/04/2014 10:56
Je suis junior des piqûres de rappel ne font jamais de mal! Merci pour les tutos si tu en as d'autres sous le coude j'ai hâte de les lire!
Encore !!!!
Soumis par Mathieu G. le Mardi 15/04/2014 20:19
Merci.
N'hésite pas à t'abonner au flux RSS ou à la newsletter pour être tenu informé lorsqu'il y aura de nouveaux tutos.
A bientôt.
Bien expliqué mais une question me turlupine
Soumis par AnteusEx le Mercredi 18/03/2015 14:11
Bonjour,
Je viens de tomber (oui, un an après...) sur cet article fort intéressant et bien expliqué.
J'ai une question par rapport à ce que vous avez dit sur l'implémentation du jeu :
Idéalement il faudrait que des développeurs externes au projet puissent proposer de nouvelles races sous forme d'addons sans toucher aux fichiers de l'application (afin de maîtriser la qualité du jeu et de rendre possible l'utilisation de plusieurs addons simultanément)
Dans le dernier diagramme, la classe main est fortement couplée avec les différent type d'usine qui correspondrait à l'ajout d'une nouvelle "race".
Ainsi, ajouter une nouvelle race va forcément demander à modifier la classe main ou toute classe client de la classe "Usine".
Ai-je bien compris ou suis-je complètement à côté ?
Merci par avance,
Bonne journée
Bien expliqué mais une question me turlupine
Soumis par Mathieu G. le Mercredi 18/03/2015 22:59
Bonjour,
Vous avez tout à fait raison, dans cet exemple, ajouter une race nécessitera de modifier la classe Main car fortement couplée aux usines concrétes.
Mais il s'agit d'un exemple volontairement simple pour illustrer l'utilisation du design pattern Fabrique.
Pour résoudre le problème que vous évoquez on pourrait faire en sorte que la classe Main lise un fichier de configuration (xml par exemple) qui liste les usines concrêtes utilisables.
De ce fait, il suffirait d'ajouter une ligne dans ce fichier de configuration et ainsi ne pas modifier l'implémentation de la classe Main.
A bientôt.
Bien expliqué mais une question me turlupine
Soumis par AnteusEx le Jeudi 19/03/2015 12:24
Bonjour,
Merci pour cette réponse clair.
Du coup, une autre question m'est venue.
Est-ce une abération que d'imbriquer plusieurs pattern fabrique les uns dans les autres ? Pas une plétore, mais au moins deux ?
Ou alors il serait plus efficace de coupler le pattern fabrique avec d'autres pattern afin d'avoir une architecture plus solide et résistante au changement ?
A bientôt
Bien expliqué mais une question me turlupine
Soumis par Mathieu G. le Samedi 27/02/2016 14:54
Bonjour,
Je ne vois pas trop ce que vous voulez dire par imbriquer plusieurs fois le pattern fabrique.
Si votre question est toujours d'actualité, auriez-vous un exemple à me proposer en s'inspirant de mon jeu ci-dessus ?
Merci.
Excellent article :
Soumis par fatma b le Mardi 10/01/2017 21:58
Bonjour,
Je vous remércie pour votre article détaillé.
J'ai une question , est ce que le design patttern fabrique est applicable dans le contexte d'une application fondé sur Spring?
Merci pour vos lumiéres,
Fatma
Ajouter un commentaire