Aller au menu - Aller au contenu

Icône Les limites de l'héritage : le pattern strategy

Mise à jour : 12/02/2010
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
96 702 visites depuis 7 jours, dont 598 sur ce chapitre classé 4/786
Comme je vous le disais dans l'introduction de cette partie, nous allons partir du postulat que vous avez un code qui fonctionne, et par là j'entends un ensemble de classes objets liées par héritage, ou autre.

Nous allons voir dans ce chapitre que, malgré la toute puissance de l'héritage, celui-ci trouve ses limites lorsque vous êtes amenés à modifier vos hiérarchies de classes afin de répondre à une demande (votre chef, un client...).
Et le fait de toucher à votre hiérarchie peut amener des erreurs non désirables (si une erreur peut l'être...), et même des absurdités. Tout ceci dû au fait que vous allez changer une structure qui fonctionne à cause des contraintes que vous subissez.

Pour remédier à cela, il y a un concept simple : en fait, il s'agit d'un des fondements de la programmation orientée objet : l'encapsulation !

Dans ce chapitre nous parlerons de cette solution, que vous avez déjà vue sans le savoir : ceci si vous avez suivi la partie 3 du tuto.

Bon, il est temps d'y aller !
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Posons le problème

Voici le tableau



Vous êtes un jeune et ambitieux développeur d'une toute nouvelle société qui crée des jeux vidéos.
Le dernier titre en date, Z-Army, un jeu de guerre très réaliste, a été un succès interplanétaire ! Votre patron est content et vous aussi. ;)
Vous vous êtes pourtant basé sur une architecture vraiment simple afin de créer et utiliser des personnages (guerrier, médecin...). D'ailleurs, la voici :

Image utilisateur


Pour ceux qui seraient totalement étrangers à UML, qu'ils fassent un tour dans le chapitre consacré à cet effet.
Bon, vous constatez que votre hiérarchie est très simple : la classe Personnage est une classe abstraite dont héritent les classes Guerrier et Medecin.
Les guerriers savent se battre tandis que les médecins soignent les blessés sur le champs de bataille !

Les ennuis commencent maintenant ! :diable:
Votre patron vous a confié le projet Z-Army2 "The return of the revenge".
Vous vous dites : yes !... Mon architecture fonctionne à merveille, je la garde.
Et vous commencez à créer le second volet du jeu.

Un mois plus tard, votre patron vous convoque dans son bureau et vous dit : "Nous avons fait une étude de marché, et il semblerait que les joueurs aimeraient se battre aussi avec les médecins ! ".
Vous trouvez l'idée séduisante et vous avez déjà pensé à une solution : déplacer la méthode combattre() dans la super-classe Personnage, afin de pouvoir la redéfinir dans la classe Medecin et jouir du polymorphisme !

Votre diagramme de classe ressemble donc à ceci :

Image utilisateur


À la seconde étude de marché, votre patron vous annonce que vous allez devoir créer des civils, des snipers, des chirurgiens... Toute une panoplie de personnages spécialisés dans leur domaine !

Voici à présent votre diagramme de classe :

Image utilisateur


Le code source de ces classes



Personnage.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public abstract class Personnage {

	/**
	 * Méthode de déplacement de personnage
	 */
	public abstract void seDeplacer();
	/**
	 * Méthode que les combattants utilisent
	 */
	public abstract void combattre();
}



Guerrier.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Guerrier extends Personnage {

	public void combattre() {
		System.out.println("Fusil, pistolet, couteau ! Tout ce que tu veux !");
	}

	public void seDeplacer() {
		System.out.println("Je me déplace à pied.");
	}
}



Medecin.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Medecin extends Personnage{
	public void combattre() {
		System.out.println("Vive le scalpel !");
	}

	public void seDeplacer() {
		System.out.println("Je me déplace à pied.");
	}

        public void soigner(){
		System.out.println("Je soigne les blessures.");
	}
}



Civil.java



Code : Java
1
2
3
4
5
6
7
8
9
public class Civil extends Personnage{
	public void combattre() {
		System.out.println("Je ne combats PAS !");
	}

	public void seDeplacer() {
		System.out.println("Je me déplace à pied.");
	}
}



Chirurgien.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Chirurgien extends Personnage{
	public void combattre() {
		System.out.println("Je ne combats PAS !");
	}

	public void seDeplacer() {
		System.out.println("Je me déplace à pied.");
	}

	public void soigner(){
		System.out.println("Je fais des opérations.");
	}
}



Sniper.java



Code : Java
1
2
3
4
5
6
7
8
9
public class Sniper extends Personnage{
	public void combattre() {
		System.out.println("Je me sers de mon fusil à lunette !");
	}

	public void seDeplacer() {
		System.out.println("Je me déplace à pied.");
	}
}


À ce stade, vous devriez remarquer que :
  • le code contenu dans la méthode seDeplacer() est dupliqué dans toutes les classes ! Il est identique dans toutes celles citées ci-dessus ;
  • le code de la méthode combattre() de la classe Chirurgien et Civil est lui aussi dupliqué !


La duplication de code est l'une des choses qui peuvent générer des problèmes dans le futur !
Je m'explique.
Pour le moment, votre chef ne vous a demandé que de créer quelques classes supplémentaires. Qu'en sera-t-il si plusieurs classes, qui n'ont que le seul lien d'héritage existant, ont ce même code dupliqué ? Il ne manquerait plus que votre chef vous demande de modifier à nouveau la façon de se déplacer de ces objets pour en oublier un, voire même plusieurs ! Et voilà les incohérences qui pointent le bout de leur nez... :o

No problemo ! Tu vas voir... Il suffit de mettre un comportement par défaut pour le déplacement et pour le combat dans la super-classe Personnage. :magicien:


Effectivement, votre idée se tient. Donc, ceci nous donne ce qui suit...

Personnage.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public abstract class Personnage {

	/**
	 * Méthode de déplacement de personnage
	 */
	public void seDeplacer(){
		System.out.println("Je me déplace à pied.");
	}
	/**
	 * Méthode que les combattants utilisent
	 */
	public void combattre(){
		System.out.println("Je ne combats PAS !");
	}
}



Guerrier.java



Code : Java
1
2
3
4
5
6
public class Guerrier extends Personnage {

	public void combattre() {
		System.out.println("Fusil, pistolet, couteau ! Tout ce que tu veux !");
	}
}



Medecin.java



Code : Java
1
2
3
4
5
6
7
8
9
public class Medecin extends Personnage{
	public void combattre() {
		System.out.println("Vive le scalpel !");
	}

        public void soigner(){
		System.out.println("Je soigne les blessures.");
	}
}



Civil.java



Code : Java
1
2
public class Civil extends Personnage{
}



Chirurgien.java



Code : Java
1
2
3
4
5
public class Chirurgien extends Personnage{
	public void soigner(){
		System.out.println("Je fais des opérations.");
	}
}



Sniper.java



Code : Java
1
2
3
4
5
public class Sniper extends Personnage{
	public void combattre() {
		System.out.println("Je me sers de mon fusil à lunette !");
	}
}


Voici une classe contenant un petit programme afin de tester nos classes :

Code : Java
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
		Personnage[] tPers = {new Guerrier(), new Chirurgien(), new Civil(), new Sniper(), new Medecin()};
		for(Personnage p : tPers){
			System.out.println("\nInstance de " + p.getClass().getName());
			System.out.println("*****************************************");
			p.combattre();
			p.seDeplacer();
		}		
	}


Et le résultat de ce code :

Image utilisateur


Apparemment, ce code vous donne ce que vous voulez !
Plus de redondance... Mais, personnellement, un problème me chiffonne. Vous ne pouvez pas utiliser les classes Medecin et Chirurgien de façon polymorphe, vu que la méthode soigner() leur est propre !
Alors, on définit un comportement par défaut (ne pas soigner) dans la super-classe Personnage et le tour est joué !

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Personnage {

	/**
	 * Méthode de déplacement de personnage
	 */
	public void seDeplacer(){
		System.out.println("Je me déplace à pied.");
	}
	/**
	 * Méthode que les combattants utilisent
	 */
	public void combattre(){
		System.out.println("Je ne combats PAS !");
	}
        /**
         * Méthode de soin
         */
	public void soigner(){
		System.out.println("Je ne soigne pas.");
	}
}


Au même moment, votre chef rentre dans votre bureau et vous dit :
"Nous avons bien réfléchi, et il serait de bon ton que nos guerriers puissent administrer les premiers soins".
À ce moment précis, vous vous délectez de votre capacité d'anticipation ! Vous savez que maintenant, il vous suffit de redéfinir la méthode soigner() dans la classe concernée et tout le monde est content ! :D

Seulement voilà ! Votre chef n'avait pas fini son speech...
"Au fait, il faudrait adapter un comportement différent à nos personnages selon leurs armes, leurs habits, leurs trousses de soin... Enfin tu vois ! Les comportements figés pour des personnages de jeux, de nos jours... c'est un peu ringard !"

Vous commencez à voir ce dont il retourne ! Vous allez apporter des modifications à votre code, encore et encore...
Problème : à chaque modification de comportement de vos personnages, vous êtes obligés de modifier le code source de la classe concernée !


Bon : pour un programmeur, ceci est le train-train quotidien, j'en conviens.
Cependant, si nous suivons les consignes de notre chef et que nous continuons sur notre lancée, les choses vont se compliquer... Voyons voir.

Un problème supplémentaire

Attelons-nous à appliquer les modifications dans notre programme.
Si nous suivons les consignes de notre chef, et c'est ce que nous allons faire, nous allons devoir gérer des comportements différents selon les accessoires de nos personnages.
En fait, nous pouvons utiliser des variables d'instance et utiliser celles-ci pour appliquer tel ou tel comportement.

Afin de simplifier l'exemple, nous n'allons utiliser que des objets String.


Voici le diagramme de classes de notre programme :

Image utilisateur


Vous avez remarqué que nos personnages vont avoir des accessoires. Selon ceux-ci, nos personnages feront des choses différentes.
Voici les recommandations de notre chef bien-aimé :
  • le guerrier devra pouvoir utiliser un couteau, un pistolet ou un fusil de sniper ;
  • le sniper peut utiliser son fusil de sniper mais aussi un fusil à pompe ;
  • le médecin a une trousse simple pour soigner mais peut utiliser un pistolet;
  • le chirurgien a une grosse trousse médicale mais ne peut pas utiliser d'arme ;
  • le civil, quant à lui, peut utiliser un couteau seulement quand il en a un ;
  • tous les personnages hormis le chirurgien peuvent avoir des baskets pour courir.

Il va nous falloir des accesseurs pour ces variables, mettons-les dans la super-classe ! ;)
Inutile de mettre les méthodes de renvoi (get), nous ne nous servirons que des mutateurs !


Bon, les modifications sont faites, les caprices de notre cher et tendre chef sont satisfaits ? Voyons ça tout de suite.

Personnage.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public abstract class Personnage {

	protected String armes = "", chaussure = "", sacDeSoin = "";
	/**
	 * Méthode de déplacement de personnage
	 */
	public void seDeplacer(){
		System.out.println("Je me déplace à pied.");
	}
	/**
	 * Méthode que les combattants utilisent
	 */
	public void combattre(){
		System.out.println("Je ne combats PAS !");
	}
	/**
     * Méthode de soin
     */
	public void soigner(){
		System.out.println("Je ne soigne pas.");
	}
	
	protected void setArmes(String armes) {
		this.armes = armes;
	}
	protected void setChaussure(String chaussure) {
		this.chaussure = chaussure;
	}
	protected void setSacDeSoin(String sacDeSoin) {
		this.sacDeSoin = sacDeSoin;
	}
}



Guerrier.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Guerrier extends Personnage {

	public void combattre() {
		if(this.armes.equals("pistolet"))
			System.out.println("Attaque au pistolet !");
		else if(this.armes.equals("fusil de sniper"))
			System.out.println("Attaque au fusil de sniper !");
		else
			System.out.println("Attaque au couteau !");
	}
}



Sniper.java



Code : Java
1
2
3
4
5
6
7
8
public class Sniper extends Personnage{
	public void combattre() {
		if(this.armes.equals("fusil à pompe"))
			System.out.println("Attaque au fusil à pompe !");
		else
			System.out.println("Je me sers de mon fusil à lunette !");
	}
}



Civil.java



Code : Java
1
2
3
4
5
6
7
8
public class Civil extends Personnage{
	public void combattre(){
		if(this.armes.equals("couteau"))
			System.out.println("Attaque au couteau !");
		else
			System.out.println("Je ne combats PAS !");
	}
}



Medecin.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Medecin extends Personnage{
	public void combattre() {
		if(this.armes.equals("pistolet"))
			System.out.println("Attaque au pistolet !");
		else
			System.out.println("Vive le scalpel !");
	}
	
	public void soigner(){
		if(this.sacDeSoin.equals("petit sac"))
			System.out.println("Je peux recoudre des blessures.");
		else
			System.out.println("Je soigne les blessures.");
	}
}



Chirurgien.java



Code : Java
1
2
3
4
5
6
7
8
public class Chirurgien extends Personnage{
	public void soigner(){
		if(this.sacDeSoin.equals("gros sac"))
			System.out.println("Je fais des merveilles.");
		else
			System.out.println("Je fais des opérations.");
	}
}


Voici un programme de test :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void main(String[] args) {
		Personnage[] tPers = {new Guerrier(), new Chirurgien(), new Civil(), new Sniper(), new Medecin()};
		String[] tArmes = {"pistolet", "pistolet", "couteau", "fusil à pompe", "couteau"}; 
		for(int i = 0; i < tPers.length; i++){
			System.out.println("\nInstance de " + tPers[i].getClass().getName());
			System.out.println("*****************************************");
			tPers[i].combattre();
			tPers[i].setArmes(tArmes[i]);
			tPers[i].combattre();
			tPers[i].seDeplacer();
			tPers[i].soigner();
		}		
	}


Et le résultat de ce test :

Image utilisateur


Vous constatez avec émerveillement que votre code fonctionne très bien. Les actions par défaut sont respectées, les affectations d'actions aussi. Tout est parfait !
Vraiment ? Vous êtes sûr de ça ?
Je ne vois pas ce qui cloche ! o_O

Pourtant, je vois du code dupliqué dans certaines classes ! :colere2:
En plus, nous n'arrêtons pas de modifier nos classes sans arrêt...
Lors de Z-Army1, celles-ci étaient pourtant très bien ! Qu'est-ce qui ne va pas ? Je ne comprends pas !

Là-dessus, votre patron rentre dans votre bureau pour vous dire que : "les actions de vos personnages devront pouvoir êtres utilisables à la volée et, en fait, les personnages pouvaient très bien apprendre au fil du jeu..."

Et là, inutile de demander un congé à votre patron pour cause de migraine ! :p
Les changements s'accumulent, votre code devient de moins en moins lisible et réutilisable, bref, l'enfer sur terre.

Faisons un point sur la situation :
  • du code dupliqué s'insinue dans votre code ;
  • à chaque modification de comportement, vous êtes obligés de modifier le code source de la (ou des) classe(s) concernée(s) ;
  • votre code perd en réutilisabilité et, du coup, votre code n'est pas extensible du tout !

Extensible ? Tu entends quoi par là ?

Par là j'entends que vos objets, sortis de leurs contexte, ne pourront plus être réutilisés. Ils auront été modelés pour l'application que vous êtes en train de programmer. Ceci est dû au fait que nous avons utilisé l'héritage à outrance...

Problème : le fait est que dans notre programme, les personnages sont liés entre eux mais ceux-ci ont des comportements tellement différents que nous ne savons pas quoi en faire ! Nous avons essayé de placer ces derniers à différents endroits dans notre hiérarchie mais le problème persiste, au final...


Voyons comment résoudre ce problème. :)

Une solution simple et robuste : le pattern strategy

Après toutes ces émotions, vous allez enfin avoir une solution à ce problème de modification de code source !
Si vous vous souvenez de ce que j'ai dit dans l'introduction, un des fondements de la programmation orientée objet est : l'encapsulation !

Le pattern strategy est basé sur ce principe simple.

L'encapsulation est un mécanisme visant à rassembler des données et / ou des méthodes au sein d'une structure spécifique.
Ces méthodes et / ou ces données sont ainsi réutilisables partout ailleurs dans le programme.

Je me doute que cette phrase semble pompeuse... Mais remplacez "une structure" par "un objet".
Vous devez mieux comprendre le sens de cette phrase, non ?

Bon, vous avez compris que le pattern strategy consiste à créer des objets avec des données et / ou méthodes.
Oui, on comprend bien, mais lesquelles ?

Tout simplement ce qui change dans votre programme !

Le principe de base de ce pattern est le suivant :
isolez ce qui varie dans votre programme et encapsulez-le !
Désolé, mais on ne comprend toujours pas... :(

Pas de panique, nous allons y aller doucement.

Déjà, quels sont les éléments qui ne cessent de varier dans notre programme ?
  • La méthode combattre() ;
  • la méthode seDeplacer() ;
  • la méthode soigner().

Nous avons tenté, en vain, de déployer ces comportements dans notre hiérarchie de classe, mais sans grand succès dû aux problèmes cités plus haut...
Ce qui serait vraiment grandiose, ce serait d'avoir la possibilité de ne modifier que les comportements et non les objets qui ont ces comportements !


Là, je vous arrête un moment. Vous venez de fournir la solution de vive voix. Vous avez dit : "Ce qui serait vraiment grandiose, ce serait d'avoir la possibilité de ne modifier que les comportements et non les objets qui ont ces comportements".

Lorsque je vous ai présenté les diagrammes UML, je vous ai fourni une astuce pour bien différencier les liens entre les objets. Dans notre cas, nos classes héritant de Personnage héritent aussi des ses comportements et, par conséquent, on peut dire que nos classes filles sont des Personnage.
Concernant les comportements de la classe mère, ils semblent ne pas être au bon endroit dans la hiérarchie. Vous ne savez plus quoi en faire et vous vous demandez s'ils ont vraiment leur place dans cette classe ?

Il vous suffit de sortir ces comportements de la classe mère, de créer une classe abstraite ou une interface symbolisant ce comportement, et de dire à votre classe Personnage d'avoir ces comportements.
Voici mon nouveau diagramme de classes :

Image utilisateur


Ouh là ! Qu'est-ce que c'est que toutes ces interfaces ?

Je me doutais un peu que vous fronceriez les sourcils... ^^
N'oubliez pas que votre code doit être souple et robuste et, même si ce chapitre vous montre les limites de l'héritage, n'oubliez pas que le polymorphisme est inhérent à l'héritage (et aux implémentations d'interfaces).
Il faut que vous vous rendiez compte qu'utiliser une interface de cette manière revient à créer un super-type de variable et, du coup, nous pourrons utiliser les classes héritant de ces interfaces de façon polymorphe, sans se soucier de savoir de quelle classe nos objets sont issus !
Dans notre cas, nous allons avoir des objets de type EspritCombatif, Soin et Deplacement dans notre classe Personnage !

Nous pouvons résumer la situation comme ceci : dans nos hiérarchies de classes, il est parfois préférable de privilégier la composition (= "a un") à l'héritage (= "est un") [cf. chapitre sur UML].
Les comportements susceptibles d'être trop difficiles à généraliser dans la classe mère pourront ainsi être isolés en créant un nouveau type d'objet (encapsulation) correspondant à chaque comportement !


Avant de nous lancer dans le codage de nos nouvelles classes, vous devez vous rendre compte que leur nombre a considérablement augmenté depuis le début de ce chapitre.
Afin de pouvoir y voir plus clair et ainsi gagner en clarté, nous allons gérer nos différentes classes avec différents packages.
Comme je vous l'avais déjà dit dans un précédent chapitre, un package est un dossier comprenant plusieurs classes ou dossiers en son sein. Les classes sont regroupées par utilité ou par thème.

L'un des avantages de faire ceci est que nous allons gagner en lisibilité dans notre package par défaut mais aussi que les classes mises dans un package sont plus facilement transportables d'une application à l'autre. Pour cela, il vous suffira d'inclure le dossier de votre package dans un projet et d'importer les classes qui vous intéressent ! :magicien:

Comment crée-t-on un nouveau package ?

Ah, ce n'est pas difficile du tout, il vous suffit de cliquer sur cette icône :

Image utilisateur


Une boîte va s'ouvrir vous demandant le nom de votre package :

Image utilisateur


Attention : il existe aussi une convention de nommage pour les packages !
  • Ceux-ci doivent être écrits entièrement en minuscules.
  • Les caractères doivent être de a à z, de 0 à 9 et un point (.).
  • Sun indique que tout package doit commencer par : com, edu, gov, mil, net, org ou les deux lettres identifiants un pays (ISO Standard 3166, 1981) donc, fr => France, eng => England...



Nous sommes sur le Site du Zér0, j'ai donc pris le nom à l'envers : sdz.com => com.sdz.
Par exemple, mes packages ont tendance à s'appeler com.cysboy.<nom>.

Bon, fin de l'aparte.

Cliquez sur "Finish" pour créer le package. Ensuite, il ne vous reste plus qu'à créer les interfaces de notre diagramme de classe dans ce package (clic droit dessus, "new / interface").
Et voilà ! Votre package est prêt à l'emploi :

Image utilisateur


Ce sera donc dans ce package que nous allons développer nos comportements !
Vous pouvez très bien déclarer un package par comportement... Comme si vous rangiez des dossiers dans un classeur ! ;)


Doucement ! Tu ne pourrais pas plutôt nous expliquer un peu plus tout ce mic-mac avec tes interfaces... o_O

J'allais justement le faire.
Comme nous l'avons remarqué tout au long de ce chapitre, les comportements de nos personnages sont trop épars pour être définis dans notre super-classe Personnage. Vous l'avez dit vous-mêmes, il faudrait que l'on ne puisse modifier que les comportements et non les classes héritant de notre super-classe !
Les interfaces nous servent à créer un super-type d'objet ; ainsi, nous utiliserons des objets de type :
  • EspritCombatif qui ont une méthode combat() ;
  • Soin qui ont une méthode soigne() ;
  • Deplacement qui ont une méthode deplace().

Dans notre classe Personnage, nous avons ajouté une instance de chaque type de comportement, vous avez dû le remarquer : il y a ces attributs dans notre schéma ! ^^
Nous allons développer un comportement par défaut pour chaque type de comportement et nous allons affecter cet objet dans notre super-classe. Les classes filles, elles, auront des instances différentes, correspondant à leur besoin.
Du coup, que fait-on des méthodes de la super-classe Personnage ?

Nous les gardons, mais, au lieu d'avoir une redéfinition de ces dernières, la super-classe va invoquer la méthode de comportement de chaque objet. Ainsi, nous n'avons plus à redéfinir ou à modifier nos classes ! La seule chose qu'il vous reste à faire, c'est d'affecter une instance de comportement à vos objets.

Vous comprendrez mieux avec un exemple. Voici quelques implémentations de comportements.

Implémentations de l'interface EspritCombatif



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class Pacifiste implements EspritCombatif {
	public void combat() {
		System.out.println("Je ne combats pas ! ");
	}
}


Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class CombatPistolet implements EspritCombatif{
	public void combat() {
		System.out.println("Je combats au pitolet !");
	}
}



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class CombatCouteau implements EspritCombatif {	
	public void combat() {
		System.out.println("Je me bats au couteau !");
	}
}



Implémentations de l'interface Deplacement



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class Marcher implements Deplacement {
	public void deplacer() {
		System.out.println("Je me déplace en marchant.");
	}
}



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class Courir implements Deplacement {
	public void deplacer() {
		System.out.println("Je me déplace en courant.");
	}
}


Implémentations de l'interface Soin



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class PremierSoin implements Soin {
	public void soigne() {
		System.out.println("Je donne les premiers soins.");
	}
}



Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class Operation implements Soin {
	public void soigne() {
		System.out.println("Je pratique des opérations !");
	}
}


Code : Java
1
2
3
4
5
6
7
package com.sdz.comportement;

public class AucunSoin implements Soin {
	public void soigne() {
		System.out.println("Je ne donne AUCUN soin !");
	}
}


Voici ce que vous devriez avoir dans votre nouveau package :

Image utilisateur


Les classes mises dans un package et destinées à être utilisées à l'extérieur du dit package DOIVENT être déclarées public ! !
Sinon, les classes ne seront visible qu'à l'intérieur du package et vous ne pourrez pas les utiliser !


Maintenant que nous avons défini des objets de comportements, nous allons pouvoir remanier notre classe Personnage.
Nous allons ajouter les variables d'instances, des mutateurs et des constructeurs afin de pouvoir initialiser nos objets.

Voici la nouvelle version de notre super-classe :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import com.sdz.comportement.*;

public abstract class Personnage {

	//Nos instances de comportements
	protected EspritCombatif espritCombatif = new Pacifiste();
	protected Soin soin = new AucunSoin();
	protected Deplacement deplacement = new Marcher();
	
	/**
	 * Constructeur par défaut
	 */
	public Personnage(){}
	
	/**
	 * Constructeur avec paramètres
	 * @param espritCombatif
	 * @param soin
	 * @param deplacement
	 */
	public Personnage(EspritCombatif espritCombatif, Soin soin,
			Deplacement deplacement) {
		this.espritCombatif = espritCombatif;
		this.soin = soin;
		this.deplacement = deplacement;
	}
	/**
	 * Méthode de déplacement de personnage
	 */
	public void seDeplacer(){
		//On utilise les objets de déplacement de façon polymorphe
		deplacement.deplacer();
	}
	/**
	 * Méthode que les combattants utilisent
	 */
	public void combattre(){
		//On utilise les objets de déplacement de façon polymorphe
		espritCombatif.combat();
	}
	/**
     * Méthode de soin
     */
	public void soigner(){
		//On utilise les objets de déplacement de façon polymorphe
		soin.soigne();
	}
	
	//************************************************************
	//						ACCESSEURS
	//************************************************************
	
	/**
	 * Redéfinit le comportement au combat
	 * @param espritCombatif
	 */
	protected void setEspritCombatif(EspritCombatif espritCombatif) {
		this.espritCombatif = espritCombatif;
	}
	/**
	 * Redéfinit le comportement de Soin
	 * @param soin
	 */
	protected void setSoin(Soin soin) {
		this.soin = soin;
	}
	/**
	 * Redéfinit le comportement de déplacement
	 * @param deplacement
	 */
	protected void setDeplacement(Deplacement deplacement) {
		this.deplacement = deplacement;
	}	
}


Il y a eu du changement depuis le début... Mais maintenant, nous n'utilisons plus des méthodes définies dans notre hiérarchie de classe, mais des implémentations concrètes d'interfaces !
Les méthodes que nos objets appellent utilisent chacune d'elle un objet de comportement. Nous pouvons donc définir des guerriers, des civils, des médecins... tous personnalisables puisqu'il suffit de changer leurs instances de comportements pour que les comportements de ceux-ci changent instantanément. La preuve en image.

Je ne vais pas vous donner les codes de toutes les classes... En voici seulement quelques-unes.

Guerrier.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sdz.comportement.*;

public class Guerrier extends Personnage {	
	/**
	 * Constructeur par défaut
	 */
	public Guerrier(){
		this.espritCombatif = new CombatPistolet();
	}
	/**
	 * Constructeur personnalisé
	 * @param espritCombatif
	 * @param soin
	 * @param deplacement
	 */
	public Guerrier(EspritCombatif espritCombatif, Soin soin,
			Deplacement deplacement) {
		//Appel au constructeur de la super classe
		super(espritCombatif, soin, deplacement);
	}
}


Civil.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import com.sdz.comportement.*;

public class Civil extends Personnage{

	public Civil() {}

	public Civil(EspritCombatif espritCombatif, Soin soin,
			Deplacement deplacement) {
		super(espritCombatif, soin, deplacement);
	}
	
}


Medecin.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import com.sdz.comportement.*;

public class Medecin extends Personnage{

	public Medecin() {
		this.soin = new PremierSoin();
	}

	public Medecin(EspritCombatif espritCombatif, Soin soin,
			Deplacement deplacement) {
		super(espritCombatif, soin, deplacement);
	}
	
}


N'oubliez pas d'importer le package contenant nos classes de comportement !


Maintenant, voici un exemple d'utilisation :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Test{
	public static void main(String[] args) {
		Personnage[] tPers = {new Guerrier(), new Civil(), new Medecin()};
		
		for(int i = 0; i < tPers.length; i++){
			System.out.println("\nInstance de " + tPers[i].getClass().getName());
			System.out.println("*****************************************");
			tPers[i].combattre();
			tPers[i].seDeplacer();
			tPers[i].soigner();
		}		
	}
}


Le résultat de ce code nous donne :

Image utilisateur


Vous pouvez voir que nos personnages ont tous un comportement par défaut qui leur conviennent bien !
Nous avons spécifié, dans le cas où c'est nécessaire, le comportement par défaut d'un personnage dans son constructeur par défaut :
  • le guerrier se bat avec un pistolet ;
  • le médecin soigne.


Or, voyons comment dire à nos personnages de faire autre chose... Que diriez-vous de faire faire une petite opération chirurgicale à notre objet Guerrier ? Pour ce faire, vous pouvez redéfinir son comportement de soin avec son mutateur, présent dans la super-classe :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sdz.comportement.*;


class Test{
	public static void main(String[] args) {
		Personnage[] tPers = {new Guerrier(), new Civil(), new Medecin()};
		
		for(int i = 0; i < tPers.length; i++){
			System.out.println("\nInstance de " + tPers[i].getClass().getName());
			System.out.println("*****************************************");
			tPers[i].combattre();
			tPers[i].seDeplacer();
			tPers[i].soigner();
			if(tPers[i].getClass().getName().equals("Guerrier")){
				tPers[i].setSoin(new Operation());
				System.out.print(" \t Après modification de comportement de soin : \n \t\t");
				tPers[i].soigner();
			}
		}		
	}
}


Ce qui nous donne :

Image utilisateur


Vous voyez que le comportement de soin de notre objet a changé dynamiquement, sans que nous ayons besoin de changer la moindre ligne de son code source ! :magicien:
Le plus beau dans le fait de travailler comme ceci, c'est que vous pouvez tout à fait instancier des Guerrier avec des comportements différents très simplement, mais vous pouvez aussi leur donner des comportements que vous codez à la volée !

Regardez ceci :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.sdz.comportement.*;


class Test{
	public static void main(String[] args) {
		Personnage civil = new Civil();
		System.out.println("Comportement par défaut d'un civil : ");
		System.out.println("****************************************");
		civil.combattre();
		civil.soigner();
		civil.seDeplacer();
		
		System.out.println("\nTransformation d'un civil : ");
		System.out.println("****************************************");
		civil.setDeplacement(new Deplacement(){
			public void deplacer() {
				System.out.println("Je saute sur tout ce qui bouge ! ! ! !");
			}			
		});
		civil.setSoin(new Soin(){
			public void soigne() {
				System.out.println("L'amputation est ma grande passion ! ! !");
			}
		});
		civil.setEspritCombatif(new EspritCombatif(){
			public void combat() {
				System.out.println("Je roule en char d'assaut ! ! ATTENTION DEVANT ! ! !");
			}
		});
		
		civil.combattre();
		civil.soigner();
		civil.seDeplacer();
		
	}
}


Ce qui donne, au final :

Image utilisateur


Vous avez pu constater que vous n'avez plus de code dupliqué ! :magicien:
Les modifications de comportement deviennent très simples à faire et vous n'avez plus à modifier le code source de votre classe Personnage en cas de changements...

Je suppose que, maintenant que vous avez vu cet exemple, vous avez deviné où et quand vous avez utilisé le pattern strategy !
Lorsque vous programmiez des implémentations de ActionListener pour la gestion de vos événements... ;)
Sauf que, dans ce cas, il y a une nuance. Vous avez utilisé le pattern strategy pour créer des comportements lors d'événements sur votre IHM, mais ces interfaces de gestion d'événements sont utilisées dans un autre pattern : le pattern observer !
Nous aborderons ce dernier très bientôt...

Bon, je crois qu'un petit topo s'impose...

Ce qu'il faut retenir

  • Les design pattern sont des modèles de conception permettant de créer des programmes souples et faciles à maintenir.
  • Le pattern strategy permet de rendre une hiérarchie hermétique à la modification tout en lui permettant d'avoir des comportements différents.
  • La base de ce DP réside dans l'encapsulation.
  • Vous devez isoler les parties qui ont tendance à trop changer dans vos codes et les encapsuler.
  • L'action ci-dessus permet de ne pas avoir de code dupliqué dans vos applications car les comportements sont encapsulés !
  • Ceci peut être résumé ainsi : dans certains cas, vous devrez préférer la composition (= "a un") à l'héritage (= "est un").
Voici un chapitre qui a dû vous montrer l'héritage sous un autre jour !
Vous pouvez voir qu'il y a des solutions simples à utiliser et qui vous permettent d'avoir un code "hermétique à la modification".

Le but final, c'est de n'avoir à modifier que l'endroit dans lequel vous utilisez vos objets et non vos objets eux-mêmes, et ce pattern fait ça très bien ! :D

Nous avons vu comment faire en sorte de modifier les comportements de vos objets de façon dynamique, nous allons maintenant voir comment rajouter des fonctionnalités à vos objets dynamiquement.
En route pour le pattern decorator !
Chapitre précédent Sommaire Chapitre suivant

Partager

18 commentaires pour "Les limites de l'héritage : le pattern strategy"
Note moyenne : 3.57 / 4 (1025 votes)
Pseudo Commentaire
Hors ligne IgorLeCochon # Posté le 01/07/2010 à 11:24:21

Les erreurs sont dues au fait que le code des interfaces ne sont pas donnés.

Il faut créer les 3 interfaces 'Soin, EspritCombattif et Deplacement), et déclarer leur méthode. Et là, comme sur des roulettes (j'ai testé)
Hors ligne IgorLeCochon # Posté le 01/07/2010 à 11:26:39

(désolé du double post)

J'ai oublié, un grand merci à cysboy du boulot qu'il a fait.
Meme si certain chipotte sur certain point qui peuvent etre mis à jour, il n'ont qu'a le faire, c'est facile ed critiquer !

Un grand merci !
Hors ligne Sladur # Posté le 16/01/2011 à 23:41:56

Pour compléter parfaitement ce tuto, ne faudrait-il pas rajouter des factory pour choisir quelle implémentation de l'interface correspond au style du personnage ( sniper, guerrier, ect ...). Plutôt que ça soit la classe elle même qui décide de quelle implémentation elle va utilisée, surtout que tu as séparé les comportements du package principale ( qui contient les différents personnage).

Ainsi les dépendance entre package ne se ferait que via une factory et une interface. Je pense même que pour optimiser tout ça ( optimisation en terme d'encapsulation),il faudrait faire une interface de cette factory qui posséderait comme constante une instance de son implémentation. Cette serait donnée par un fichier .properties où alors tout simplement en dur dans le code. Ainsi les dépendances entre package se ferait uniquement via des interfaces.

C'est vrai que faire l'interface de la factory serait un peu poussé, mais je pense qu'expliquer le principe de la factory serait assez intéressant dans ce cas ci je trouve.
Hors ligne kiwixou # Posté le 01/02/2011 à 23:08:23

C'est intéressant, je ne savais pas que ce genre de modélisation était un "design pattern" :o
Même si c'est pas le but du tuto qui très bien expliqué, on pourrait pousser un poil plus le vice de l'héritage en faisant en sorte qu'un chirurgien "étende" un médecin de la même manière qu'un sniper "étend" un guerrier, l'un et l'autre étant une version spécialisée de leur parent.
Hors ligne Aktarel # Posté le 11/08/2011 à 23:01:24

Études : ESIEA Paris

Bien revoir les notions de polymorphisme/héritage! Moyen très astucieux de contourner les limites de l'héritage avec un concept de génie logiciel avancé! ;P

Voir tous les commentaires