Langage de programmation:
Design pattern: 

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

Diagramme UML de l'implémentation de la Simple Fabrique en Java

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 console

Consomme 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

Diagramme UML de l'implémentation en Java du 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 console

Consomme 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,

Code source: 
Commentaires

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.

 

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.

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.

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 !

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!

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

 

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

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