Dans le chapitre précédent, nous avons présenté les bases de la théorie des classes. Nous allons maintenant introduire des notions complémentaires, qui vous permettront de structurer votre code encore plus facilement et proprement. Contrairement à la plupart des notions de cette partie sur la POO, celles que nous allons découvrir dans ce chapitre n'ont pas été présentées dans le premier chapitre, et sont donc complètement nouvelles. Redoublez donc d'attention !
- Les éléments statiques
- Une nouvelle sorte de « variable » : la constante !
- Un objet dans un objet (dans un objet...)
- Exercice : Jeu de rôle
Les éléments statiques
Peut-être que certains se rappellent de la classe Math
que nous avons déjà utilisé. Ils se demandent alors pourquoi nous l'avions utilisé en utilisant directement le nom de la classe et sans passer par une instance de celle-ci :
1 | var monNombre:Number = Math.sqrt(2); |
Comment cela est-ce possible ?
Maintenant que vous avez déjà quelques connaissances en POO, il est tout à fait justifié de se poser ce genre de question. En fait, la classe Math
utilise des éléments qui sont un peu particuliers : les éléments statiques !
Comme nous le verrons, ces éléments statiques ne sont pas définis pour les instances d'une classe, mais pour la classe elle-même. Il existe deux types d'éléments statiques qui sont :
- les variables statiques
- les méthodes statiques.
Nous verrons donc comment créer ces éléments et quel est leur intérêt.
Les variables statiques
Les variables statiques sont déclarées à l'aide du mot-clé static
, et sont associées donc définies pour la classe.
Prenons l'exemple de notre classe Voiture
du chapitre précédent, et ajoutons-y une variable statique représentant le nombre de fois où celle-ci a été instanciée :
1 | public static var occurences:int = 0; |
Cette variable est donc partagée par la classe, elle n'appartient pas aux instances de celle-ci. Toutefois cette variable est accessible depuis n'importe quel point de la classe. Notamment, nous pourrions incrémenter cette variable à l'intérieur du constructeur de la classe Voiture
afin de comptabiliser le nombre d'occurrences de celle-ci :
1 | occurrences++; |
Grâce à cette variable statique, nous pourrions obtenir le nombre d'instances de la classe Voiture
, n'importe où dans le code. Pour cela, nullement besoin de créer une nouvelle instance de la classe, il suffit d'utiliser le nom de la classe lui-même :
1 2 3 4 | var uneRenault:Voiture = new Voiture("Renault"); var unePeugeot:Voiture = new Voiture("Peugeot"); var uneCitroen:Voiture = new Voiture("Citroën"); trace(Voiture.occurrences); // Affiche : 3 |
Un élément statique ne peut être utilisé qu'avec la classe où celui-ci est déclaré. Il est impossible de faire référence à un élément statique à l'aide d'une instance de cette classe : des erreurs seraient alors engendrées.
Les méthodes statiques
Il existe un second type d'éléments statiques : il s'agit des méthodes statiques. Dans le chapitre précédent, je vous avais dit que les méthodes servaient principalement à la lecture ou à la modification d'un attribut. Nous pouvons donc introduire les méthodes statiques comme l'ensemble des méthodes qui offrent des fonctionnalités n'affectant pas au moins l'un des attributs d'une classe.
Ces éléments statiques sont également déclarés à l'aide du mot-clé static
:
1 2 3 4 | public static function uneMethode():void { // Instructions } |
À l'aide de ces méthodes statiques, il nous est possible de recréer la classe Math
, que nous pourrions renommer MaClasseMath
. Voici par exemple la redéfinition de la méthode pow()
en puissance()
:
1 2 3 4 5 6 7 8 9 | public static function puissance(nombre:int, exposant:int):int { var resultat:int = nombre for(var i:int = 1; i < exposant; i++) { resultat *= nombre; } return resultat; } |
Le code complet de la classe serait :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package { public class MaClasseMath { public static function puissance(nombre:int, exposant:int):int { var resultat:int = nombre for(var i:int = 1; i < exposant; i++) { resultat *= nombre; } return resultat; } } } |
Vous remarquerez que cette classe ne possède pas de constructeur : en effet, il est permis de ne pas mettre de constructeur si vous ne vous en servirez pas. Ici, nous n'avons pas mis de constructeur car nous n'allons jamais créer d'instance de cette classe.
Nous pouvons ainsi l'utiliser sans créer d'occurrences de cette nouvelle classe :
1 2 | var monNombre:int = MaClasseMath.puissance(2,10); trace(monNombre); // Affiche : 1024 |
Des classes telles que Math
ont été conçues pour être utilisées uniquement grâce à des éléments statiques. En utilisant ce principe, vous pouvez ainsi regrouper un ensemble de fonctionnalités à l'intérieur d'une même classe. Vous n'aurez donc pas besoin de créer d'occurrences de celle-ci et vous ferez ainsi l'économie des instructions de déclarations et d'initialisations des instances.
Il est impossible d'utiliser le mot-clé this
dans les méthodes statiques, car elles ne sont liées à aucun objet en particulier.
Une nouvelle sorte de « variable » : la constante !
Lorsque nous avons introduit les variables dans la première partie, nous n'avons pas parlé des constantes ! Comme son nom l'indique, la valeur d'une constante est figée contrairement à celle d'une variable qui est vouée à évoluer au cours du programme. Ces constantes sont principalement utilisées en POO, et représentent des caractéristiques constantes d'un objet.
Je vous invite à découvrir ce nouveau type d'élément sans plus attendre !
Présentation
Déclaration
De la même façon que nous avions l'instruction ou mot-clé var
pour déclarer une variable, nous disposons du mot-clé const
en ce qui concerne les constantes. Ces dernières possèdent également un type, de la même manière que les variables.
Voici par exemple la déclaration d'une constante de type String
:
1 | const MA_CONSTANTE:String; |
Vous remarquerez qu'ici nous n'utilisons pas la notion Camel. Effectivement, il est de coutume d'écrire les noms de constantes en lettres majuscules. Cela permet de les différentier des variables, et de préciser qu'il s'agit bien d'une constante.
Utilisez l'underscore « _
» pour séparer les différents mots à l'intérieur du nom de votre constante.
Le code précédent n'a malheureusement aucun intérêt et ne sert à rien sans l'initialisation de la constante !
Initialisation
Tout comme une variable, il serait bien d'initialiser une constante. Vous pouvez procéder exactement de la même manière que pour une variable. La technique d'initialisation dépend bien entendu du type de la constante.
Voici donc comment initialiser notre constante précédente de type String
:
1 | const MA_CONSTANTE:String = "Valeur"; |
Contrairement aux variables, Il est strictement impossible d'initialiser la valeur d'une constante ailleurs que lors de sa déclaration. Étant donné que la valeur d'une constante est non modifiable, n'essayez pas non plus de procéder à une affectation.
Intérêt des constantes
Il y a certainement plus de la moitié, voire même les trois quarts d'entre vous qui se sont posé la question suivante :
À quoi ces constantes peuvent-elles bien servir ?
Contrairement à ce que vous pensez, les constantes ont plusieurs utilités.
- Tout d'abord, elles permettent de mettre des noms sur des valeurs. Votre programme ne marchera pas mieux avec cela, c'est uniquement une question de clarification du code. Avouez qu'il est quand même plus aisé de comprendre la signification d'une expression, si celle-ci utilise des noms plutôt que des valeurs :
prixRoue * NOMBRE_DE_ROUES
plutôt queprixRoue * 4
. Dans le second cas, nous pourrions nous demander s'il s'agit d'une augmentation du prix d'une roue, une conversion du prix des euros aux dollars, ou bien une multiplication par le nombre de roues. Dans la première expression, l'opération est tout à fait claire ; ce qui simplifie grandement le travail de relecture d'un code. - Une autre utilité des constantes est de s'assurer de la pérennisation du code. Imaginez que le nombre de roues de votre voiture puisse servir à plusieurs calculs comme le prix de l'ensemble, son poids, … Vous devrez donc utiliser cette valeur plusieurs fois dans votre code et à des endroits différents. Ainsi en utilisant une constante à la place de la valeur réelle, vous facilitez une éventuelle mise à jour de votre programme dans l'hypothèse de l'invention de la voiture à 6 roues ! Essayez d'imaginer le travail qu'il aurait fallu fournir pour remplacer chacune des valeurs présentes dans des coins opposés de votre code.
N'utilisez pas non plus des constantes à tour de bras dans vos programmes. Leur but est de simplifier la lecture du code ; n'allez donc pas le compliquer davantage en remplaçant n'importe quelle valeur par une constante !
Un objet dans un objet (dans un objet...)
Jusqu'à présent, nous n'avions utilisé qu'une seule classe à la fois. Mais là où la POO devient vraiment intéressante, c'est lorsque nous combinons les classes entre elles !
Le problème du pétrole
Reprenons la classe Voiture
:
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 | package { public class Voiture { /*************** Attributs ***************/ private var _marque:String; private var _couleur:String; private var _longueur:int; private var _immatriculation:String; /************* Constructeur *************/ public function Voiture(marque:String) { _marque = marque; _couleur = "Sans couleur"; _longueur = -1; _immatriculation = "Non immatriculée"; } /*************** Accesseurs ***************/ public function get marque():String { return _marque; } public function get couleur():String { return _couleur; } public function get longueur():int { return _longueur; } public function get immatriculation():String { return _immatriculation; } /*************** Mutateurs ***************/ public function set couleur(nouvelleCouleur:String):void { _couleur = nouvelleCouleur; } public function set longueur(nouvelleLongueur:int):void { _longueur = (nouvelleLongueur > 0) ? nouvelleLongueur : -1; } public function set immatriculation(nouvelleImmatriculation:String):void { _immatriculation = nouvelleImmatriculation; } } } |
Nous voulons à présent que nos objets contiennent de l'essence. Pour cela, nous serions tenté de procéder ainsi :
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | package { public class Voiture { /*************** Attributs ***************/ private var _marque:String; private var _couleur:String; private var _longueur:int; private var _immatriculation:String; private var _typeEssence:String; private var _prixEssence:Number; private var _quantiteEssence:Number; /************* Constructeur *************/ public function Voiture(marque:String) { _marque = marque; _couleur = "Sans couleur"; _longueur = -1; _immatriculation = "Non immatriculée"; _typeEssence = "Sans Plomb"; _prixEssence = 1.4; // Euros par litre _quantiteEssence = 10; // Litres } /*************** Accesseurs ***************/ public function get marque():String { return _marque; } public function get couleur():String { return _couleur; } public function get longueur():int { return _longueur; } public function get immatriculation():String { return _immatriculation; } public function get typeEssence():String { return _typeEssence; } public function get prixEssence():Number { return _prixEssence; } public function get quantiteEssence():Number { return _quantiteEssence; } /*************** Mutateurs ***************/ public function set couleur(nouvelleCouleur:String):void { _couleur = nouvelleCouleur; } public function set longueur(nouvelleLongueur:int):void { _longueur = (nouvelleLongueur > 0) ? nouvelleLongueur : -1; } public function set immatriculation(nouvelleImmatriculation:String):void { _immatriculation = nouvelleImmatriculation; } public function set typeEssence(nouveauType:String):void { _typeEssence = nouveauType; } public function set prixEssence(nouveauPrix:Number):void { _prixEssence = nouveauPrix; } public function set quantiteEssence(nouvelleQuantite:Number):void { _quantiteEssence = nouvelleQuantite; } } } |
Notre classe commence à devenir compliquée ; il vaudrait mieux créer une nouvelle classe pour partager les propriétés.
Une nouvelle classe
Créons une classe Essence
à mettre dans le fichier Essence.as
:
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 | package { public class Essence { /*************** Attributs ***************/ private var _type:String; private var _prix:Number; private var _quantite:Number; /************* Constructeur *************/ public function Essence() { _type = "Sans Plomb"; _prix = 1.4; // Euros par litre _quantite = 10; // Litres } /*************** Accesseurs ***************/ public function get type():String { return _type; } public function get prix():Number { return _prix; } public function get quantite():Number { return _quantite; } /*************** Mutateurs ***************/ public function set type(nouveauType:String):void { _type = nouveauType; } public function set prix(nouveauPrix:Number):void { _prix = nouveauPrix; } public function set quantite(nouvelleQuantite:Number):void { _quantite = nouvelleQuantite; } } } |
Nous transférons donc toutes les propriétés relatives à l'essence de la voiture dans la nouvelle classe. Il va falloir maintenant adapter la classe Voiture
:
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 | package { public class Voiture { /*************** Attributs ***************/ private var _marque:String; private var _couleur:String; private var _longueur:int; private var _immatriculation:String; private var _carburant:Essence; // Nouvel attribut pointant sur un objet de classe Essence ! /************* Constructeur *************/ public function Voiture(marque:String) { _marque = marque; _couleur = "Sans couleur"; _longueur = -1; _immatriculation = "Non immatriculée"; _carburant = new Essence(); // Nous créons un objet Essence par défaut dans le constructeur } /*************** Accesseurs ***************/ public function get marque():String { return _marque; } public function get couleur():String { return _couleur; } public function get longueur():int { return _longueur; } public function get immatriculation():String { return _immatriculation; } public function get carburant():Essence // Nouvel accesseur, renvoyant un objet de classe Essence { return _carburant; } /*************** Mutateurs ***************/ public function set couleur(nouvelleCouleur:String):void { _couleur = nouvelleCouleur; } public function set longueur(nouvelleLongueur:int):void { _longueur = (nouvelleLongueur > 0) ? nouvelleLongueur : -1; } public function set immatriculation(nouvelleImmatriculation:String):void { _immatriculation = nouvelleImmatriculation; } public function set carburant(nouveauCarburant:Essence):void // Nouveau mutateur, affectant un objet de classe Essence { _carburant = nouveauCarburant; } } } |
Comme vous pouvez le constater, nous pouvons écrire des attributs pointant sur des objets. Nous pourrions même mettre un attribut de type Voiture
!
1 | private var ancienneVoiture:Voiture; // L'ancienne voiture du propriétaire |
Pour modifier le carburant de notre voiture, il faut procéder ainsi :
1 2 3 | var maVoiture = new Voiture("Peugeot"); maVoiture.carburant.type = "Diesel"; trace("Type de carburant : " + maVoiture.carburant.type); // Affiche : Type de carburant : Diesel |
Vous remarquerez que nous procédons de la même façon que pour toutes les propriétés, en utilisant le caractère point « . », comme nous l'avons vu dans le premier chapitre cette partie. Il suffit donc de mettre un point à chaque fois que nous voulons accéder à la propriété d'un objet :
- une première fois lorsque nous voulons accéder à la propriété
carburant
de notre objetmaVoiture
, - une seconde fois lorsque nous voulons modifier le type du carburant de la voiture.
Pour résumer la situation, je vous propose un petit schéma UML des classes Voiture
et Essence
que nous venons de créer :
En réalité, nous combinons depuis le début la classe Voiture
et la classe String
: beaucoup de nos attributs sont du type String
. Rappelez-vous : les chaînes de caractères sont aussi des objets !
Exercice : Jeu de rôle
Présentation de l'exercice
Le combat final contre le grand Méchant approche ! Votre personnage, son épée légendaire au poing, se dresse devant cet immense monstre armé jusqu'aux dents !
Le moment est venu de faire un peu de pratique ! Je propose la réalisation d'un petit programme ressemblant à un jeu de rôle, afin de bien revoir les notions du chapitre.
L'objectif de cet exercice est de créer la ou les classes nécessaires au bon fonctionnement du programme principal (que nous adapterons si besoin). Voici le déroulement de ce programme :
- Nous créons un objet représentant votre personnage, puis nous l'armons.
- Nous créons de façon similaire l'objet représentant le Méchant.
- Le Méchant attaque une fois votre personnage.
- Votre personnage riposte et attaque une fois le Méchant.
Pour apporter un peu de piment à ce programme, les personnages peuvent succomber aux attaques, et leur arme peut infliger un coup critique (elle a des chances d'infliger le double de dégâts à l'adversaire).
Nous allons passer par trois étapes que j'ai nommée solutions, pour voir progressivement comment programmer correctement en Orienté-Objet : à chaque étape, nous améliorerons notre code.
Il ne s'agit pas d'un TP : nous allons programmer ensemble et progressivement.
Solution initiale
Création de la classe
Commençons par créer notre première classe : j'ai choisi de l'appeler Personnage
. En effet, ce sera la classe des objets représentant nos deux personnages (vous et le Méchant).
Dans un nouveau fichier appelé Personnage.as
, écrivons la structure de base de notre classe : le package, la classe et le constructeur :
1 2 3 4 5 6 7 8 9 10 11 12 13 | package { public class Personnage { // Constructeur public function Personnage() { } } } |
Les attributs
Ensuite, ajoutons les attributs de la classe ; mais pour cela, réfléchissons les données utiles pour notre combat. Comme dans tous les jeux avec des combats (ou presque), donnons un niveau de santé à nos personnages, que nous initialiserons à 100. Et pour les armer, indiquons la puissance de l'attaque qu'ils vont porter à leur adversaire, ainsi que les chances de coup critique :
1 2 3 4 5 6 7 8 | // Santé du personnage private var _sante:int; // Dégâts de base private var _degats:int; // Chances de faire une critique (sur 100) private var _chanceCritique:int; |
Les accesseurs
N'oublions pas d'accompagner les attributs de leurs accesseurs :
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 | public function get sante():int { return _sante; } public function set sante(value:int):void { _sante = value; } public function get degats():int { return _degats; } public function set degats(value:int):void { _degats = value; } public function get chanceCritique():int { return _chanceCritique; } public function set chanceCritique(value:int):void { _chanceCritique = value; } |
Le contructeur
Ensuite, initialisons nos attributs au sein du constructeur :
1 2 3 4 5 6 7 | // Constructeur public function Personnage() { sante = 100; degats = 0; chanceCritique = 0; } |
La méthode
Enfin, ajoutons une méthode, afin que nos personnages puissent attaquer un autre personnage :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public function attaquer(cible:Personnage):void { var degatsAppliques:int = degats; // On jette un dé à 100 faces : si le résultat est inférieur ou égal à la chance de coup critique, l'attaque fait un coup critique ! if (Math.random() * 100 <= chanceCritique) { trace("Critique !"); // On double les dégâts ! degatsAppliques *= 2; } // On applique les dégâts cible.sante -= degatsAppliques; if (cible.sante <= 0) { trace("La cible est décédée."); } else { trace("Il reste " + cible.sante + " PV à la cible."); } } |
Comme vous pouvez le constater, nous passons en paramètre un objet de la classe Personnage
, afin de rendre le code logique et surtout très lisible. Ainsi, pour qu'un personnage attaque un second, il faudra procéder ainsi :
1 | personnageA.attaquer(personnageB); // Le personnageA attaque le personnageB ! S'en est fini de lui ! |
La classe complète
Si tout se passe bien, vous devriez normalement avoir une classe Personnage
qui correspond à la description ci-dessous :
Voici le code complet de notre classe Personnage
, pour vérifier le vôtre :
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 75 76 77 78 | package { public class Personnage { // Santé du personnage private var _sante:int; // Dégâts de base private var _degats:int; // Chances de faire une critique (sur 100) private var _chanceCritique:int; public function Personnage() { sante = 100; degats = 0; chanceCritique = 0; } public function get sante():int { return _sante; } public function set sante(value:int):void { _sante = value; } public function get degats():int { return _degats; } public function set degats(value:int):void { _degats = value; } public function get chanceCritique():int { return _chanceCritique; } public function set chanceCritique(value:int):void { _chanceCritique = value; } public function attaquer(cible:Personnage):void { var degatsAppliques:int = degats; // On jette un dé à 100 faces : si le résultat est inférieur ou égal à la chance de coup critique, l'attaque fait un coup critique ! if (Math.random() * 100 <= chanceCritique) { trace("Critique !"); // On double les dégâts ! degatsAppliques *= 2; } // On applique les dégâts cible.sante -= degatsAppliques; if (cible.sante <= 0) { trace("La cible est décédée."); } else { trace("Il reste " + cible.sante + " PV à la cible."); } } } } |
Le programme principal
Votre classe Main
vide (contenue dans le fichier Main.as
) devrait ressembler à cela :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package { import flash.display.Sprite; import flash.events.Event; public class Main extends Sprite { public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point } } } |
Rappel : il faut commencer à programmer après le commentaire // entry point
(«point d'entrée») à la ligne 17.
Commençons par déclarer la variable qui pointera vers le premier objet de classe Personnage
(celui qui vous représente) :
1 | var moi:Personnage = new Personnage(); |
Ensuite, donnons-lui son épée légendaire (elle fait 80 dégâts de base et a 80 chances sur 100 de faire un coup critique) :
1 2 | moi.degats = 80; moi.chanceCritique = 80; |
Le code pour créer le Méchant est très similaire :
1 2 3 | var mechant:Personnage = new Personnage(); mechant.degats = 40; mechant.chanceCritique = 10; |
Enfin, simulons le combat épique qui a lieu entre nos deux personnages ! Si vous vous souvenez de ma remarque sur la méthode attaquer()
un peu plus haut, vous savez comment procéder :
1 2 3 4 5 | trace("Le méchant m'attaque ! "); mechant.attaquer(moi); trace("Il va connaître ma fureur ! A l'attaque !"); moi.attaquer(mechant); |
Voici le code complet de notre classe Main :
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 | package { import flash.display.Sprite; import flash.events.Event; public class Main extends Sprite { public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point // Création du personnage vous représentant var moi:Personnage = new Personnage(); moi.degats = 80; moi.chanceCritique = 80; // Création du personnage Méchant var mechant:Personnage = new Personnage(); mechant.degats = 40; mechant.chanceCritique = 10; // Simulation du combat trace("Le méchant m'attaque ! "); mechant.attaquer(moi); trace("Il va connaître ma fureur ! A l'attaque !"); moi.attaquer(mechant); } } } |
Résultat
Nous pouvons maintenant compiler et tester le projet. Voici ce que donne la console :
1 2 3 4 5 6 | Le méchant m'attaque ! Critique ! Il reste 20 PV à la cible. Il va connaître ma fureur ! A l'attaque ! Critique ! La cible est décédée. |
Gagné !
Attendez malheureux ! Ne criez pas victoire trop vite ! En effet, notre classe pourrait être améliorée…
Vous ne voyez pas en quoi ? Et bien, pensez au chapitre précédent : «Un objet dans un objet (dans un objet…)». Maintenant, réfléchissez à cette problématique : comment pourrait-on mieux séparer les données et les propriétés de ma classe Personnage
? En créant de nouvelles classes, pardi !
Une nouvelle classe
En effet, il serait judicieux de représenter les armes que portent nos personnages par des objets à part entière : cela semble logique, et cela respecte les principes de la POO. En outre, cela nous faciliterait énormément la tâche si nous devions gérer un inventaire par exemple : nous pourrions mettre autant d'objets que l'on veut, et équiper nos personnages avec, tout ceci de façon très souple et naturelle !
La classe Arme
L'idée est donc de transférer toutes les propriétés relatives aux armes dans une nouvelle classe Arme
, comme ceci :
Il nous faudra donc créer une nouvelle classe (ici dans le fichier Arme.as
) :
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 | package { public class Arme { // Dégâts de l'arme private var _degats:int; // Chances de faire un coup critique (sur 100) private var _chanceCritique:int; public function Arme() { degats = 0; chanceCritique = 0; } public function get chanceCritique():int { return _chanceCritique; } public function set chanceCritique(value:int):void { _chanceCritique = value; } public function get degats():int { return _degats; } public function set degats(value:int):void { _degats = value; } } } |
La classe Personnage
N'oublions pas d'adapter la classe Personnage
, comme nous l'avons fait dans le chapitre précédent :
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 | package { public class Personnage { // Santé du personnage private var _sante:int; // Arme équipée private var _armeEquipee:Arme; // Nouvel attribut pointant sur l'arme équipée public function Personnage() { sante = 100; } public function get sante():int { return _sante; } public function set sante(value:int):void { _sante = value; } public function get armeEquipee():Arme // Nouvel accesseur { return _armeEquipee; } public function set armeEquipee(value:Arme):void // Nouveau mutateur { _armeEquipee = value; } public function attaquer(cible:Personnage):void { // Au cas où aucun arme n'est équipée (l'objet armeEquipee est null) if (armeEquipee == null) { trace("Aucune arme équipée : l'attaque échoue."); } else { var degatsAppliques:int = armeEquipee.degats; // Désormais, on utilise les propriétés de l'objet armeEquipee if (Math.random() * 100 <= armeEquipee.chanceCritique) // Ici aussi, on utilise les propriétés de l'objet armeEquipee { trace("Critique !"); // On double les dégâts ! degatsAppliques *= 2; } // On applique les dégâts cible.sante -= degatsAppliques; if (cible.sante <= 0) { trace("La cible est décédée."); } else { trace("Il reste " + cible.sante + " PV à la cible."); } } } } } |
Le programme principal
Là aussi, il va falloir adapter un peu : au lieu d'affecter directement les dégâts et les chances de critique aux personnages, nous créons dans un premier temps les armes via des objets de classe Arme
, pour ensuite les équiper aux personnages :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var epeeLegendaire:Arme = new Arme(); epeeLegendaire.degats = 80; epeeLegendaire.chanceCritique = 50; var hacheDeGuerre:Arme = new Arme(); hacheDeGuerre.degats = 40; hacheDeGuerre.chanceCritique = 10; var moi:Personnage = new Personnage(); moi.armeEquipee = epeeLegendaire; var mechant:Personnage = new Personnage(); mechant.armeEquipee = hacheDeGuerre; trace("Le méchant m'attaque ! "); mechant.attaquer(moi); trace("Il va connaître ma fureur ! A l'attaque !"); moi.attaquer(mechant); |
Avouez, le code est quand même plus clair que le précédent !
Résultat
Et voici le résultat à la console lorsque l'on teste le projet :
1 2 3 4 5 | Le méchant m'attaque ! Il reste 60 PV à la cible. Il va connaître ma fureur ! A l'attaque ! Critique ! La cible est décédée. |
Rien n'a vraiment changé (à part ma chance qui s'est envolée) : ce n'est toutefois pas pour rien que nous avons modifié notre code. En effet, il est primordial de programmer correctement pour que vos projets soient lisibles, facilement modifiables et maintenables.
Malheureusement, notre code pose encore problème : il ne respecte pas bien le principe d'encapsulation. Si vous regardez bien la méthode attaquer()
, nous utilisons des propriétés de la classe Arme
et reproduisons son comportement (à savoir : les coups critiques) dans la classe Personnage
: en toute logique, si une arme devrait faire un coup critique, nous devrions le déterminer dans la bonne classe, autrement dit la classe Arme
!
La bonne solution
La bonne façon de procéder consiste donc à appliquer les dégâts qui vont être fait dans la classe Arme
. Pour cela, créons dans cette classe une nouvelle méthode frapper()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public function frapper(cible:Personnage):void { var degatsAppliques:int = degats; // On jette un dé à 100 faces : si le résultat est inférieur ou égal à la chance de coup critique, l'attaque fait un coup critique ! if (Math.random() * 100 <= chanceCritique) { trace("Critique !"); // On double les dégâts ! degatsAppliques *= 2; } // On applique les dégâts cible.sante -= degatsAppliques; } |
Il va donc falloir appeler cette nouvelle méthode dans la méthode attaquer()
de la classe Personnage
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public function attaquer(cible:Personnage):void { if (armeEquipee == null) { trace("Aucune arme équipée : l'attaque échoue."); } else { armeEquipee.frapper(cible); // Nous appelons la nouvelle méthode ici if (cible.sante <= 0) { trace("La cible est décédée."); } else { trace("Il reste " + cible.sante + " PV à la cible."); } } } |
Les classes
L'un des intérêts de l'utilisation de la représentation UML est justement de faciliter cette étape de conception et d'organisation des différentes classes d'un même programme. Cela permet de visualiser la structure d'un projet en ne faisant ressortir que les informations utiles, et ainsi programmer plus rapidement et de manière plus propre.
Au final, vos classes devraient ressembler à ceci :
Voici le code complet de nos classes :
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 | package { public class Personnage { // Santé du personnage private var _sante:int; // Amre équipée private var _armeEquipee:Arme; public function Personnage() { sante = 100; } public function get sante():int { return _sante; } public function set sante(value:int):void { _sante = value; } public function get armeEquipee():Arme { return _armeEquipee; } public function set armeEquipee(value:Arme):void { _armeEquipee = value; } public function attaquer(cible:Personnage):void { if (armeEquipee == null) { trace("Aucune arme équipée : l'attaque échoue."); } else { armeEquipee.frapper(cible); if (cible.sante <= 0) { trace("La cible est décédée."); } else { trace("Il reste " + cible.sante + " PV à la cible."); } } } } } |
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 | package { public class Arme { // Dégâts de l'arme private var _degats:int; // Chances de faire un coup critique (sur 100) private var _chanceCritique:int; public function Arme() { degats = 0; chanceCritique = 0; } public function get chanceCritique():int { return _chanceCritique; } public function set chanceCritique(value:int):void { _chanceCritique = value; } public function get degats():int { return _degats; } public function set degats(value:int):void { _degats = value; } public function frapper(cible:Personnage):void { var degatsAppliques:int = degats; // On jette un dé à 100 faces : si le résultat est inférieur ou égal à la chance de coup critique, l'attaque fait un coup critique ! if (Math.random() * 100 <= chanceCritique) { trace("Critique !"); // On double les dégâts ! degatsAppliques *= 2; } // On applique les dégâts cible.sante -= degatsAppliques; } } } |
Le programme
Le programme principal ne change pas par rapport à la solution précédente :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var epeeLegendaire:Arme = new Arme(); epeeLegendaire.degats = 80; epeeLegendaire.chanceCritique = 50; var hacheDeGuerre:Arme = new Arme(); hacheDeGuerre.degats = 40; hacheDeGuerre.chanceCritique = 10; var moi:Personnage = new Personnage(); moi.armeEquipee = epeeLegendaire; var mechant:Personnage = new Personnage(); mechant.armeEquipee = hacheDeGuerre; trace("Le méchant m'attaque ! "); mechant.attaquer(moi); trace("Il va connaître ma fureur ! A l'attaque !"); moi.attaquer(mechant); |
Résultat
Enfin, voici le résultat de l'exécution de notre programme :
1 2 3 4 | Le méchant m'attaque ! Il reste 60 PV à la cible. Il va connaître ma fureur ! A l'attaque ! Il reste 20 PV à la cible. |
En résumé
- Il est possible d'utiliser des éléments dits statiques qui sont directement liés à la classe et non à ses instances.
- Ces éléments statiques sont déclarés à l'aide du mot-clé
static
et facilitent l'ajout de fonctionnalités au programme. - Il est impossible d'utiliser le mot-clé
this
dans les méthodes statiques. - Nous pouvons créer des constantes qui sont similaires aux variables, mais qui ne peuvent pas être modifiées.
- Pour déclarer une constante, nous devons utiliser le mot-clé
const
. - Il est possible de combiner les classes entre elles : les objets peuvent alors contenir d'autres objets.