Salut tout le monde ! On se retrouve aujourd’hui pour parler à nouveau de programmation orientée objet ! Lors du dernier article à ce sujet, on a vu le principe des classes et leur structure, maintenant que l’on est familier avec le concept d’objet, on va s’amuser à imbriquer les classes les unes dans les autres. Je tiens à vous recommander de prendre un café, juste au cas où…
Pourquoi faire ?
Prenons un cas pratique : imaginons que l’on crée un jeu dans lequel il y a un système de combat (oui c’est le même exemple que dans l’introduction, d’ailleurs on va se re-servir du code). On pourrait imaginer un système d’attaque avec plusieurs armes différentes. Ca donnerai une classe de ce genre :
1 2 3 4 5 | class Hache { var pointsAttaque: Int = 5 var nom: String = "Hache" } |
On pourrait créer une variable stockant l’arme sur le joueur :
1 2 3 4 5 6 7 8 | class Personnage { var nom : String = "Bobby" var pts_vie : Int = 20 var position : [Int] = [0, 0] var attaque : Int = 3 var arme = Hache() } |
Puis de servir se l’arme pour l’attaque :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Personnage { var nom : String = "Bobby" var pts_vie : Int = 20 var position : [Int] = [0, 0] var attaque : Int = 3 var arme = Hache() func seDeplacer() { print("Déplacement") } func attaquer() { let points = arme.pointsAttaque let nom = arme.nom print("Attaque de " + String(points) + " points de dégat avec " + nom) } func subiAttaque() { print("Attaque reçue") } } |
Si vous n’avez pas compris ce qui vient de se passer vous devriez (re)lire l’introduction à la POO
Ca marche. On peut le vérifier en mettant tout ça dans un playgrounds et en exécutant la fonction attaquer() de notre personnage.
Mais tout se complique si l’on veut ajouter une seconde arme au jeu avec une propriété différente, par exemple une épée qui rend de la vie lorsqu’on l’utilise. Pour pouvoir faire ça, on est obligé de créer une fonction propre à l’arme qui sera exécutée lorsque l’on se sert de cette arme, et contenant un code rendant de la vie à son porteur. Quelque chose comme ça :
1 2 3 4 5 6 7 8 9 | class Epee { var pointsAttaque: Int = 1 var pointsVie = 3 var nom: String = "Epee" func attaque() { print("Rends " + String(pointsVie) + " points de vie") } } |
Le problème qui se pose est que si on souhaite équiper l’épée comme arme sur le joueur, Xcode va nous retourner une erreur de type : Cannot assign value of type ‘Epee’ to type ‘Hache’. Il n’est tout simplement pas possible de changer le type de la variable arme qui a été défini par Hache(). Il faut donc que les deux armes possèdent le même type tout en ayant du code différent les unes des autres.
L’héritage unique
On va tout simplement attribuer des types aux classes. On a vu dans les bases des variables que toutes les variables possédaient un type défini et non modifiable permettant de savoir quelles opérations peuvent s’y appliquer. Dans le cas d’une classe on dit qu’elle hérite d’une autre.
Cet héritage permet en quelque sorte de copier-coller le code de la super classe (celle qui lègue son type) dans la nouvelle classe. On peut donc utiliser un code commun à plusieurs classes en ne modifiant que ce qui est nécessaire à chaque fois et conserver la possibilité d’être intégré à un autre système par un type commun.
On va pouvoir déclarer le type d’une classe de la même façon qu’on le fait pour une variable, on le note après le nom de la classe précédé de deux points :
1 2 3 4 5 6 7 8 9 10 11 | class Arme { } class Hache: Arme { } class Epee: Arme { } |
De cette façon on obtient deux classes différentes contenant du code distinc mais partageant un type et par conséquent des propriétés communes. Il est important de noter qu’une classe ne peut se conformer qu’a un seul autre type, il est en revanche possible que la classe de cet autre type possède elle même un troisième type. Imaginons que l’on se dise que notre joueur a un sac à dos et qu’il peut ranger ses armes dedans, on pourrait dire que les armes doivent par conséquent se conformer à un type d’objet exécutant du code propre à ce rangement. La classe arme hériterai donc d’une autre classe, par exemple ObjectSacADos :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class ObjectSacADos { } class Arme: ObjectSacADos { } class Hache: Arme { } class Epee: Arme { } |
Le code de ObjetSacADos est « transmis » à la classe Arme, et donc aux classes Hache et Epee elles-mêmes.
Se servir des fonctions
Bon c’est super on sait comment donner tout plein de types à nos classes, mais on n’a toujours pas vraiment vu comment s’en servir. Comprenons un peu comment ça se passe :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class ObjetSacADos { func sacADos() { print("sac à dos") } } class Arme: ObjetSacADos { func arme() { print("arme") } } class Hache: Arme { func hache() { print("hache") } } |
Ici on a créé une fonction unique dans chaque classe. Si on utilise la classe hache, elle sera en mesure d’appeler les 3 comme si on les avait écrite à l’intérieur. Ce code revient à la même chose que celui-ci :
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Hache { func sacADos() { print("sac à dos") } func arme() { print("arme") } func hache() { print("hache") } } |
Sauf qu’on a perdu les types, ce qui nous ramène au problème initial ce qui n’est pas le but de la manoeuvre.
Réécrire les fonctions
Que se passe-t-il maintenant si on veut donner deux codes différents pour la même fonction ? C’était bien ce que l’on voulait faire au départ, et pour ce faire nous allons enlever la classe ObjetSacADos de notre code pour le rendre plus clair :
1 2 3 4 5 6 7 8 9 10 11 | class Arme { func attaquer() { print("attaque avec arme") } } class Hache: Arme { func attaquer() { print("attaque avec hache") } } |
Dans ce cas Xcode nous donne une erreur : Overriding declaration requires an ‘override’ keyword qui nous indique que l’on ne peut pas re-déclarer une fonction sans utiliser le mot clé override.
Il nous suffit donc d’ajouter ce mot-clé avant la déclaration afin de pouvoir la réécrire :
1 2 3 4 5 6 7 8 9 10 11 | class Arme { func attaquer() { print("attaque avec arme") } } class Hache: Arme { override func attaquer() { print("attaque avec hache") } } |
A ce moment là, le code de la fonction attaquer() de la classe Hache sera exécuté à la place du code de la même fonction de la classe Arme. Il reste néanmoins possible d’appeler la fonction dans notre classe héritée depuis la même fonction dans la classe héritante grâce à un mot clé particulier : super.
Super agit comme une variable que l’on a pas besoin de (et que l’on ne doit pas) déclarer au sein de notre classe Hache qui permet l’accès à la superclasse. Voici comment s’en servir :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Arme { func attaquer() { print("attaque avec arme") } } class Hache: Arme { override func attaquer() { print("attaque avec hache") super.attaquer() // On appelle la fonction attaquer() de la classe Arme } } let hache = Hache() hache.attaquer() |
Ce qui nous affichera un retour dans la console :
1 2 | attaque avec hache attaque avec arme |
Grâce à ça on a la possibilité de décomposer son code en fonction du role de chaque classe. Par exemple on pourrait utiliser la fonction attaquer de la superclasse pour infliger les points de dégât car ce sera un code commun à toutes les armes du jeu, et réécrire cette fonction pour chaque arme pour laquelle c’est nécessaire y ajoutant diverses autres actions…
Le cas de init() et les variables
Bien ! On avance pas trop mal et on est presque arrivé à ce que l’on voulait faire au début (et oui je vous rappelle que l’on voulait ajouter une arme qui rende de la vie lorsqu’on s’en sert à la base 😄). Ecrivons ensemble la classe Arme :
1 2 3 4 5 6 7 8 9 | class Arme { var pointsAttaque: Int var nom: String init(pointsAttaque pts : Int, nom _nom : String) { pointsAttaque = pts nom = _nom } } |
On est obligé d’utiliser init() (cf : introduction à la POO) pour définir les valeurs des variables propres à l’arme, en d’autres termes on ôte la possibilité d’une arme avec un nom et des points d’attaque par défaut, ils seront toujours spécifiques.
On serai tenté pour créer notre hache à partir de là de directement déclarer les variables avec leurs valeurs propres dans la classe Hache, un peu comme ça :
1 2 3 4 | class Hache: Arme { var pointsAttaque: Int = 5 var nom = "Hache" } |
Sauf qu’il est impossible d’overrider les variables de la superclasse, même si elles n’ont pas de valeur par défaut. Il est donc impératif de déclarer de nouveau la fonction init() de la classe Hache, cette fois ci sans argument :
1 2 3 4 5 6 7 8 9 | class Hache: Arme { init() { let pts = 5 let _nom = "Hache" super.init(pointsAttaque: pts, nom: _nom) } } let hache = Hache() |
De cette manière, on peut déclarer une nouvelle instance de la classe hache par un simple appel de la classe, sans passer d’arguments. Rappelons nous que deux fonctions peuvent avoir le même nom si elles ne prennent pas les mêmes arguments on peut donc avoir deux initializers, l’un sur la classe Arme prenant en paramètre les valeurs des variables propres à l’arme, et l’autre dans la classe Hache ne prenant aucun argument et dont le seul role et de « passer » les informations à la superclasse. Cela nous évite d’avoir à redéfinir des valeurs identiques à chaque fois que l’on souhaite déclarer une Hache dans le code.
Comme je sens que votre cerveau est en train de couler par vos oreilles, je vous ai fait un schéma :
Et grâce à ça, on peut intégrer très rapidement et simplement (lol) notre hache à notre personnage. Attention à bien définir le type de la variable arme sur Arme sinon il prendra le type Hache par défaut je vous laisse donc essayer le code suivant dans un playground :
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 | class Arme { var pointsAttaque: Int var nom: String init(pointsAttaque pts : Int, nom _nom : String) { pointsAttaque = pts nom = _nom } func attaquer() { print("Attaque de " + String(pointsAttaque) + " points de dégat avec " + nom) } } class Hache: Arme { init() { let pts = 5 let _nom = "Hache" super.init(pointsAttaque: pts, nom: _nom) } } class Personnage { var nom : String = "Bobby" var pts_vie : Int = 20 var position : [Int] = [0, 0] var attaque : Int = 3 var arme: Arme = Hache() func seDeplacer() { print("Déplacement") } func attaquer() { arme.attaquer() } func subiAttaque() { print("Attaque reçue") } } let bobby = Personnage() bobby.attaquer() let bobby = Personnage() bobby.attaquer() |
Notez que nous avons ajouté la fonction attaquer() à la classe Arme afin de la rendre accessible à la classe Personnage.
Et en guise d’exercice et pour vérifier que vous ayez bien tout compris, je vous laisse créer la classe épée de vos propres mains ! Voir la correction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Epee: Arme { var pointsVieRendu: Int init() { pointsVieRendu = 3 let pts = 2 let _nom = "Epee" super.init(pointsAttaque: pts, nom: _nom) } override func attaquer() { print("Rends" + String(pointsVieRendu) + " points de vie") super.attaquer() } } |
Bon ! On a fait un grand pas en avant aujourd’hui, je vous laisse donc sur ce code. N’hésitez pas à le modifier, à rajouter des variables, les enlever, créer d’autres armes, etc… pour bien comprendre son fonctionnement et intégrer parfaitement ce principe d’héritage des classes. Quant à moi, je vous souhaite une bonne continuation et vous retrouve très bientôt pour se faire encore un peu chauffer les méninges !