Fil d'Ariane
Mis à jour le mardi 7 mars 2017
  • Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Héritage, types membres et polymorphisme

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Les classes sont l'élément de base de l'orienté objet mais elles ne sont pas tout. Une fois couplées avec l'héritage et le polymorphisme, on commence à sentir la puissance de la POO et à écrire des programmes robustes et facilement maintenables.
Le seul bémol est que des notions comme l'héritage et le polymorphisme sont difficiles à assimiler pour un débutant parce que, d'une part, elles sont très abstraites et d'autre part parce que les tutoriels et les cours sur ce sujet sont mal faits. Les auteurs de ces cours commencent par un grand pavé théorique puis ils introduisent quelques exemples, très abstraits (des voitures, des formes géométriques, des villes), et tenant sur trois lignes. Du coup, on ne comprend rien. On peut avoir l'impression d'avoir compris, mais dès qu'on se trouve obligé d'écrire nos propres programmes, on se rend compte qu'on n'a réellement rien assimilé et qu'on ne peut pas appliquer ces notions dans un cas réel.
Je ne vais pas procéder de la sorte, parce que je trouve que, pour bien assimiler l'intérêt de la POO, il vaut mieux étudier un cas réel, et introduire la théorie implicitement dans les exemples. Dans ce chapitre, nous allons nous placer dans un petit scénario où l'on vous invite à écrire un programme puis nous discuteront les différentes possibilités pour se rendre compte de l'importance des nouvelles notions introduit.

Problème

Ce matin, en me réveillant, j'ai trouvé dans ma boite mail un message venant de mon ami de jeunesse Jojo le clown. Au début j'ai cru que, comme d'habitude, il s'agit d'une invitation à son spectacle, mais ce n'était pas le cas (dommage :p ). Je vous laisse découvrir l'histoire :

Citation : Jojo le clown

Cher E++,
Comment vas-tu ? T'as fini ta prépa ?
Pour une fois, je t'envoie un message parce que j'ai besoin de ton aide. On a eu dernièrement un nouveau directeur du cirque. Il est très gentil, mais il a une phobie des clowns ! Il m'a donc forcé à changer mon spectacle, je mènerais désormais un spectacle d'animaux (des chiens, des oiseaux et des serpents). J'adore tous les animaux et j'en suis un bon dresseur, sauf que mon boss me demande de faire un programme de gestion des différents types d'animaux et de leur dressage. Mais hélas, mes connaissances en programmation sont limités au Hello World en VB (en plus mon boss n'aime pas le VB :( ).
Je n'ai pas trop d'infos sur ce que doit faire le programme, je te laisse la liberté de faire ce que tu veux. Une dernière chose, le programme doit être écrit dans un langage élégant, performant et portable si possible. J'attends ta réponse, et j'espère qu'elle viendra le plus vite possible.
Cordialement, Jojo l'ancien clown

Le pauvre Jojo, il aimait vraiment son métier. J'aurai besoin de votre aide les zéros, ensemble on finira le programme à temps. Choisissons d'abord le langage : selon Jo, il doit être "élégant, performant et portable". Scala est un bon candidat, non ?
Au boulot ! Commençons par définir la structure générale de notre programme, on doit avoir une classe Animal pour les animaux et une classe pour Jojo. Attendez ! Je ne suis pas prêt à avoir 100 clones de Jojo, un seul me suffit amplement. Il sera donc plus judicieux de représenter Jojo par un singleton. Ensuite on doit penser à ce qu'on va mettre dans nos classes. C'est simple, il suffit de savoir quel comportement on veut pour chacune d'entre elles. Un animal de cirque doit avoir un nom (rigolo de préférence), il doit savoir se déplacer, il faut qu'il soit entrainable, et, parce qu'on fait un programme, il doit savoir s'afficher à l'écran. Jojo doit juste savoir comment dresser un animal. Voici donc le code :

class Animal(val nom: String, var dresse: Boolean = false) {
  def seDeplacer = "Je me Déplace"
  def dresser = {
    dresse = true
    "J'ai été dressé !!"
  }
  def afficher = println("Je suis un animal, je m'appelle " + nom + " et " + seDeplacer)
}

object Jojo {
  def dresser(a: Animal) = a.dresser
}

Si vous avez bien suivi les chapitres précédents, vous ne devez avoir aucun souci à comprendre ce code. Faisons quelques tests :

def main(args: Array[String]) {
  val chien = new Animal("Bobby")
  val serpent = new Animal("Tika")
  chien.afficher
  Jojo.dresser(serpent)
  serpent.afficher  
}
Je suis un animal, je m'appelle Bobby et Je me Déplace
J'ai été dressé !!
Je suis un animal, je m'appelle Tika et Je me Déplace

Ça marche, mais c'est moche ! Tous les animaux ont un comportement identique, ils se déplacent de la même façon ce qui n'est pas vrai. On doit donc changer d'implémentation. Que faire ?

On peut créer une classe pour chaque type d'animal, non ?

Essayons cette approche, on n'a rien à perdre (sauf du temps et de l'énergie :-° ) :

class Chien(val nom: String, var dresse: Boolean = false) {
  def seDeplacer = "Je marche"
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un chien, je m'appelle " + nom + " et " + seDeplacer)
}

class Serpent(val nom: String, var dresse: Boolean = false) {
  def seDeplacer = "Je glisse"
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un serpent, je m'appelle " + nom + " et " + seDeplacer)
}

class Poisson(val nom: String, var dresse: Boolean = false) {
  def seDeplacer = "Je nage"
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un poisson, je m'appelle " + nom + " et " + seDeplacer)
}

Pour Jojo, une seule méthode ne peut plus faire tout le travail, on a besoin d'autant de méthodes que de classes d'animaux :

object Jojo {
  def dresser(a: Chien) = a.dresser
  def dresser(a: Serpent) = a.dresser
  def dresser(a: Poisson) = a.dresser
}

Et un "main" pour tester :

def main(args: Array[String]) = {
  val chien = new Chien("Bobby")
  val serpent = new Serpent("Tika")
  chien.afficher
  Jojo.dresser(serpent)
  serpent.afficher
}
Je suis un chien, je m'appelle Bobby et Je marche
J'ai été dressé !!
Je suis un serpent, je m'appelle Tika et Je glisse

L'affichage est meilleur mais du coup, on a trois fois plus du code que dans le premier essai. De plus, à chaque fois où on veut ajouter un nouvel type d'animal, on se trouve obligé de changer le singleton Jojo. Encore une perte de temps.
Bon récapitulons :

  • La première solution est courte mais très générale.

  • La deuxième est plus précise mais longue et pas pratique.

Laquelle choisir ? Aucune. Il existe d'autres façons pour avoir un programme à la fois assez court et suffisamment peu général. On va voir deux solutions possibles : les types membres et l'héritage.

Types membres

On a vu précédemment que les champs d'une classe sont soit des variables (mutables ou immuables) soit des méthodes. Il existe une troisième catégorie de champs : les types.
On crée un type avec le mot-clé type suivi par : le nom du type, un signe "=" et enfin "son corps".
Il y a deux catégories de types :

  • Les alias : si on veut renommer une classe (parce que son nom est trop long, par exemple).Exemple: type Chaine = String

  • Les types anonymes: ils ressemblent à des classes, sauf que les méthodes ne possèdent pas de corps (on dit que les méthodes sont abstraites).Exemple: type Multipliable = {def *(n: Double): Double}

La première catégorie étant simple, on va discuter des types anonymes. Pour qu'un objet o soit de type Multipliable, il faut qu'il ait une méthode de signature def *(n: Double): Double, c'est-à-dire qu'il soit multipliable par un Double et que le résultat de cette multiplication soit un Double aussi (toutes les classes numériques (Int, Double ...) possèdent une telle méthode). Essayons d'utiliser ce type dans une méthode :

def fois4(x: Multipliable) = x * 4
//dans la méthode 'main'
fois4(3.5) // ne compile pas !

Dès qu'en tente d'appeler la méthode "fois4", le compilateur se plaint et nous donne le message d'erreur : "type mismatch : found Double(3.5), required Multipliable" (type inadéquat : Double(3.5) trouvé, Multipliable requis). Même si 3.5 est un Multipliable (tout Double l'est, puisqu'il peut être multiplié par un double), le compilateur ne peut pas le deviner tout seul et on doit lui donner un coup de main, en utilisant la méthode asInstanceOf :

if (3.5 .isInstanceOf[Multipliable]) fois4(3.5 .asInstanceOf[Multipliable])

La méthode asInstanceOf oblige le compilateur à traiter 3.5 comme étant une instance de Multipliable. Lorsque la conversion est impossible, une erreur d'exécution est lancée. Pour éviter ce comportement, on peut vérifier si la conversion de type est possible ou non à l'aide de isInstanceOf(on vérifie ici que 3.5 est de type Multipliable avec isInstanceOf avant de le passer à fois3).

Appliquons tout ça à notre programme (le deuxième). L'idée est de créer un type anonyme "dressable" qui possède une méthode dresser dans Jojo, puisque tout ce que fait ce dernier est d'appeler les méthodes "dresser" des différentes classes.

object Jojo {
  type Dressable = {def dresser(): Unit}
  def dresser(a: Dressable) = a.dresser
}

Notre ami le clown peut maintenant dresser n'importe quel animal :

if (serpent.isInstanceOf[Jojo.Dressable]) Jojo.dresser(serpent.asInstanceOf[Jojo.Dressable])

On a donc eu un code moins bordélique que celui du deuxième essai et plus pratique que le premier, mais il reste imparfait pour les raisons suivantes :

  • Jojo (le singleton et non pas le clown) peut dresser n'importe quoi, même un schéma ;

  • On ne s'est pas débarrassé du code redondant ;

  • l'utilisation massive de is/asInstanceOf.

Il nous reste donc une dernière alternative, priez pour qu'elle marchera !

Héritage

Le voilà enfin ce vicieux héritage dont je vous ai parlé dans l'introduction.
En orienté objet, si une classe B hérite d'une autre classe A, alors elle aura touts ses champs publiques, sans avoir à réécrire une seule ligne de code (Ça sonne parfait pour notre programme, n'est-ce pas ^^ ?). L'héritage en Scala se fait avec le mot-clé extends, examinez cet exemple :

class A {
  def m = println("Une méthode de A")
}

class B extends A

object Main {
  def main(args: Array[String]) {
    val b = new B
    b.m
  }
}
Méthode de A

On a appelé la méthode "m" sur un objet de type B alors qu'elle n'a pas été déclarée explicitement dans la classe, mais, comme je vous ai dit plus haut, B a tous les champs que A. Le compilateur traduit extends A en quelques sortes en "va copier tout ce qui est publique en A et met-le dans B". A est dite classe mère et B classe fille.

Héritage et constructeurs

Ajoutons un peu de code au constructeurs de A et B (Rappel : tout ce qui n'est pas une déclaration d'un champ fait partie du constructeur principal) :

class A {
  println("constructeur de A")
  def m = println("Une méthode de A")
}

class B extends A {
  println("constructeur de B")
}

Et avec la méthode 'main' précédente on a :

constructeur de A
constructeur de B
Une méthode de A

Lors de la création d'une instance de B, le constructeur de A à été appelé avant celui de B. Pourquoi :o ? En fait il est fort possible que A initialise certains de ces champs dans son constructeur (connexion à un serveur, création de fichiers ... etc) donc l'appel de celui-ci est nécessaire pour garantir le bon fonctionnement des variables et méthodes hérités par la classe fille.
Remarquez que si l'on ajoute des arguments au constructeur primaire de A, on aura une belle erreur de compilation :

class A(val x: Int)
class B extends A
error: not enough arguments for constructor A: (x: Int)scalatesting.A.
Unspecified value parameter x.

Je vous donne la cause de cette erreur : Contrairement à C++ ou Java, en Scala la classe fille doit appeler un des constructeurs de sa classe mère (pour les raisons évoquées précédemment). Lorsqu'on écrit extends A, on essaye d'appeler le constructeur de A qui ne prend aucun argument (on peut écrire extends A() ), or, dans ce cas-là, un tel constructeur n'existe pas. Il suffit donc d'appeler explicitement un des constructeurs de A :

class A(val x: Int)
class B extends A(5)
//ou encore
class B(x: Int) extends A(x)

Impeccable !
Retournons à nos moutons animaux. Nous allons créer une nouvelle classe, Animal, de laquelle vont hériter nos classes Chien, Serpent et Oiseau :

class Animal(val nom: String, var dresse: Boolean = false) {
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un chien, je m'appelle " + nom)
}

class Chien(n: String,d: Boolean) extends Animal(n, d){
  def seDeplacer = println("Je marche")
}

class Serpent(n: String,d: Boolean){
  def seDeplacer = println("Je glisse")
}

class Poisson(n: String,d: Boolean) extends Animal(n, d){
  def seDeplacer = println("Je nage")
}

J'ai mis tout le code redondant dans la classe mère, et puisque tout sera hérité par les classes filles, les nouvelles classes sont globalement équivalent à celles écrites précédemment, avec 9 lignes de code en moins.

On a fini donc le programme ? Va vite envoyer le programme à Jojo !

Il est encore trop tôt pour faire la fête. Il ne faut pas oublier le singleton Jojo, l'utilisation des types anonymes a des défauts donc on va opter pour une autre solution : le polymorphisme.

Le Polymorphisme

Examiner cette ligne de code :

val doggy: Animal = new Chien

C'est une déclaration bizarre, on dirait une erreur sauf qu'elle compile sans problème. Ce qui se passe ici est qu'on crée une instance de la classe Chien et on la met dans une référence d'Animal. En d'autres termes, c'est un objet de type Chien déguisé en une instance d'Animal. :ninja: En fait, si B hérite de A on dit que B est un A (la réciproque est fausse, cf. la partie Héritage vs Délégation un peu plus bas) donc un Chien est un Animal. C'est très logique puisque Chien possède toute l'interface (variables + méthodes + types membres) publique de la classe Animal. Cependant faites attention, on ne peut appeler que les champs de la classe Animal (il se déguise bien notre chiot :p ) mais vous pouvez utiliser asInstanceOf[Chien] pour accéder aux autres membres.

Je commence à avoir mal à la tête, ça sert à quoi ton truc ?

Le polymorphisme jouera un grand rôle dans la simplification du singleton. On va remplacer toutes les méthodes de Jojo par une seule qui prend un Animal en argument et ensuite, au moment de l'utilisation, on va déguiser tous les animaux en des instances d'Animal :

object Jojo {
  def dresser(a: Animal) = a.dresser
}

object Main {
  def main(args: Array[String]) {
    val chien: Animal = new Chien("Bobby")
    val serpent = new Serpent("Filsy") //on peut omettre le type Animal ...
    Jojo.dresser(chien)
    Jojo.dresser(serpent) // ... dans ce cas c'est lors de l'appel que l'instance se déguise en Animal
  }
}

Et voilà, on a eu finalement un programme qui :

  • Marche, bien évidemment ;

  • N'est pas très général ;

  • Est court (en termes de lignes de codes) ;

  • Fait parfaitement ce qu'on veut.

Parfait ! Je l'envoie de suite à Jojo l'ancien clown. En attendant sa réponse, vous pouvez aller boire un café.

Abstraction

Je viens de recevoir la réponse de Jojo, lisons-la ensemble :

Citation : Jojo le Clown

Salut,
Cher E++, je ne peux te dire que bravo ! Tu m'as prouvé que t'es un grand zéro, au sens propre du mot. Mon directeur n'est pas satisfait du tout, il dit que le programme est mal-fait et qu'il lui manque plein de fonctionnalités :

  • On peut instancier la classe Animal. Mon boss l'a fait par erreur et a fallu passer 6 heures de débogage.

  • On a un spectacle à représenter et il faut qu'il soit géré par le programme : le chien danse, les poissons sautent et les serpents mangent des pizzas.

  • Les animaux doivent pouvoir manger non ?
    N'oublie pas qu'il y a des animaux carnivores et d'autres herbivores (on risque d'ajouter de nouveaux animaux à tout moment).

  • Un nouvel animal a été ajouté : Bob le hamster, il mange tout et il joue au ballon.

J'espère que tu sois capable de régler tout ça avant la nuit.
Jojo le clown.

Oh ! :'( On a un client insatisfait les amis et on doit travailler le plus rapidement possible pour finir le travail à temps. On va traiter ces problèmes dans l'ordre pour s'assurer qu'on n'a rien oublié.
Pour le premier , le boss à totalement raison. Instancier la classe Animal n'a absolument aucun sens. La solution est simple, il suffit de la déclarer comme abstraite. Personne ne peut instancier une classe abstraite, même pas le développeur:

abstract class Animal {/*le corps reste inchangé*/)

Voilà, un seul mot-clé ajouté et le problème est réglé. Passons aux choses sérieuses, on doit implémenter la possibilité de faire le spectacle. Comme d'habitude, c'est Jojo qui mène tout. On va donc commencer par procéder comme pour le dressage et voir ce que ça donne :

abstract class Animal(val nom: String, var dresse: Boolean = false) {
  def faireLeShow = "Je fais le show !!"
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un chien, je m'appelle " + nom)
}

object Jojo {
  // l'ancien code, inchangé
  def faireLeShow(a: Animal) = a.faireLeShow
}

Et le "main" d'essai :

object Main {
  def main(args: Array[String]) {
    val chien: Animal = new Chien("Bobby")
    val serpent: Animal = new Serpent("Filsy")
    Jojo.faireLeShow(chien)
    Jojo.faireLeShow(serpent)
  }
}

Pas mal, sauf que tous les animaux font la même chose, ça sera le cirque le plus ennuyeux au monde (en tout cas un cirque sans clowns serait forcément nul). On va devoir redéfinir la méthode dans les classes filles.

Euh ? o_O

Lorsque on désire attribuer à une (ou plusieurs) classe(s) fille(s) un comportement différent de celui de sa (leurs) classe mère on doit redéclarer quelques uns de ses membres. Pour redéfinir un membre, il suffit de le définir une autre fois comme si on en crée un nouveau, en précédant sa déclaration par le mot-clé override :

class A {
  val x = 2
  def f = "Bonjour"
}

class B extends A{
  override val x = 6
  override def f = "Bonsoir"
}

C'est bon on peut continuer ?
On va donc redéfinir faireLeShow dans chacune des classes Chien, Poisson et Serpent :

abstract class Animal(val nom: String, var dresse: Boolean = false) {
  def faireLeShow = println("Je ne fait rien !!")
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un chien, je m'appelle " + nom)
}

class Chien(n: String) extends Animal(n){
  def seDeplacer = println("Je marche")
  override def faireLeShow = println("Je danse !!")
}

class Serpent(n: String) extends Animal(n){
  def seDeplacer = println("Je glisse")
  override def faireLeShow = println("Je mange des pizzas !!")
}

class Poisson(n: String) extends Animal(n){
  def seDeplacer = println("Je nage")
  override def faireLeShow = println("Je saute !!")
}

Et avec le meme "main" que dans l'exemple précédent on obtient:

Je danse !!
Je mange des pizzas !!

Vous n'avez rien remarqué d'anormal ? Même si nos animaux sont déguisés en des instances d'Animal, c'est la méthode faireLeShow de la classe fille qui à été appelée !! Encore une fois, c'est le polymorphisme. :zorro: Le compilateur s'arrange pour appeler la méthode correcte dans tous les cas (si vous avez fait du C++ avant, il faut savoir qu'en Scala toutes les méthodes sont virtuelles).
Une remarque : si un membre de la classe mère doit être redéfini dans toutes les classes filles, on peut ne pas le déclarer complètement (pas de signe "=") dans la classe mère (qui doit être abstraite dans ce cas) : on dit qu'il s'agit d'un membre abstrait.
Voici quelques exemples de membres abstraits:

abstract class A {
  type T
  val x: Int
  var y: String
  def f : Unit
}

Une classe concrète qui hérite d'une autre abstraite doit redéfinir tous les membres abstraits de la classe mère.
On ne va pas rendre faireLeShow abstraite, on va l'utiliser dans les méthodes redéfinies :

abstract class Animal(val nom: String, var dresse: Boolean = false) {
  def faireLeShow = println("Je ne fais rien !!")
  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }
  def afficher = println("Je suis un chien, je m'appelle " + nom)
}

class Chien(n: String) extends Animal(n){
  def seDeplacer = println("Je marche")
  override def faireLeShow = if(dresse) println("Je danse !!") else super.faireLeShow
}

Ici on affiche deux messages différents selon est ce que l'animal est dressé ou pas. Vous savez déjà que this référence l'instance actuelle d'une classe. D'une façon analogue, super est une référence vers une instance de la classe mère. Donc super.faireLeShow appelle la méthode définie dans Animal, celle qui affiche "je ne fais rien !!".

Types membres abstraits

Dans la première version du programme on a oublié la gestion de l'alimentation des animaux. Commençons par créer les classes qui représentent les aliments :

class Nourriture
class Viande extends Nourriture
class Fromage extends Nourriture

Simple comme bonjour ;) . Le vrai problème est l'intégration des aliments dans les classes d'animaux. La solution la plus naïve serait d'ajouter une méthode manger dans Animal et de la redéfinir dans les classes filles. Mauvaise idée ! Qu'est-ce qu'on va mettre pour le type d'argument de la méthode ? Nourriture ? Ça ne marchera pas car on ne pourra plus changer le type après (une méthode redéfinie doit avoir la même signature que celle de la classe mère) ce qui permettra de faire manger du fromage à un serpent !

Donc on va opter pour la solution moche ? A savoir ajouter une méthode différente pour chaque type d'animal ?

Il n'y aura rien de moche dans ce tutoriel :pirate: . On va mettre comme type d'argument de la méthode manger un type membre abstrait qui sera implémenté dans les classes filles. On va ajouter aussi un membre booléen aFaim pour savoir si l'animal a faim ou pas :

abstract class Animal(val nom: String, var dresse: Boolean = false) {
  def faireLeShow = {
    println("Je ne fais rien !!")
  }

  def dresser = {
    dresse = true
    println("J'ai été dressé !!")
  }

  def afficher = println("Je suis un chien, je m'appelle " + nom)

  type Aliments //déclaration d'un type membre abstrait
  var aFaim = false

  private def manger(a: Aliments) = {
    aFaim = false
  }
}

class Chien(n: String) extends Animal(n){
  def seDeplacer = println("Je marche")
  override def faireLeShow =
    if(dresse && !aFaim) { //un chien qui a faim ne peut pas danser
      println("Je danse !!")
      aFaim = true  // et il doit manger après chaque spectacle
    }
  else super.faireLeShow
  override type Aliments = Viande // redéfinition du type membre Aliments
}

La méthode manger définie dans Animal prend un argument de type abstrait Aliments. Dans la classe fille Chien, et puisque nous avons redéfini Aliments, la signature de manger devient manger(aliment: Viande). Désormais, lorsque quelqu'un tente de faire manger autre chose que de la viande à un chien, il aura une erreur de compilation.
Une deuxième solution est de diviser les animaux en herbivores et carnivores, en créant deux classes filles Herbivore et Carnivore de la classe Animal, et en faisant hériter les classes Chien, Serpent et Poisson d'une de ces deux classes. Ainsi, on pourra définir une méthode "manger" dans chacune des deux classes Herbivore et Carnivore :

abstract class NouritureCarnivore
abstract class NourritureHerbivore
class Viande extends NourritureCarnivore
// ... etc

abstract class Carnivore(nom: String, dresse: Boolean = false) extends Animal(nom, dresse) {
  def manger(a: NourritureCarnivore) = { aFaim = false }
} 

abstract class Herbivore(nom: String, dresse: Boolean = false) extends Animal(nom, dresse) {
  def manger(a: NourritureHerbivore)
} 

object Jojo {
  def faireMangerH(animal: Herbivore, aliment: NourritureHerbivore) = animal.manger(aliment)
  def faireMangerC(animal: Carnivore, aliment: NourritureCarnivore) = animal.manger(aliment)
  def dresser(a: Animal) = a.dresser
}

On a été obligé à écrire deux méthodes, mais ce n'est pas très grave.

Héritage multiple

Il reste une dernière amélioration à faire : Spot le Hamster. D'abord il est le seul hamster du cirque donc il sera un singleton et non pas une classe. On n'a aucun problème ici puisque les singletons peuvent hériter et redéfinir des membres exactement comme les classes font (en fin de compte il s'agit d'une classe qui ne possède qu'une seule instance). Le réel défi est que le hamster est omnivore, c'est-à-dire à la fois herbivore et carnivore. De quelle classe entre Herbivore et Carnivore va-t-on faire hériter le singleton Hamster (en supposant qu'on a opté pour la deuxième solution), sachant qu'on ne peut hériter que d'une et une seule classe ? Si on hérite directement d'Animal, l'appel Spot.isInstanceOf[Carnivore] va retourner false, qui est un résultat inattendu, vu que tout animal omnivore est forcément carnivore. Donc faire hériter Spot directement d'Animal n'est pas une bonne idée. Heureusement, il existe en Scala un mécanisme appelé "trait" qui va nous permettre d'effectuer notre héritage multiple.

Les traits

Un trait est une classe abstraite, mais avec quelques particularités :

  • Un trait est déclaré avec le mot clé trait alors que les classes abstraites avec abstract class

  • On ne peut pas déclarer de champs dans le constructeur d'un trait, c'est-à-dire écrire val ou var avant le nom d'un argument du constructeur principal (cette restriction sera supprimée dans les prochaines versions de Scala)

  • On peut hériter de plusieurs traits.

Voyons des exemples de traits :

trait Multipliable {
  def *(x: Double)
}

trait Mechant {
  def direBonjour = println("Je vous déteste les gars ! ")
}

trait Stupide 
trait Personne { 
  val nom: String
  val age: Int
}

class Rationnel extends Multipliable {/* */}
class Prof extends Personne with Mechant
object Mark extends Personne with Mechant with Stupide

Remarquer qu'on utilise parfois with et parfois extends, ça diffère selon le cas :

  • Si la classe hérite uniquement d'un trait, on utilise extends

  • Si elle hérite d'une classe et plusieurs traits, on utilise extends pour la classe et with pour le reste

  • Si elle hérite de plusieurs traits, le premier est déclaré avec extends et les autres avec with

En d'autres termes, il n'y a jamais with sans extends, et il y a toujours au plus un seul extends. ;)

Retour au programme. Pour pallier au problème d'héritage multiple, on va créer deux traits Carnivore et Herbivore qui vont remplacer les classes héritées de Animal. Chacun de ses trait contiendra une méthode manger ayant la signature adéquate :

trait Carnivore <: Animal{
  def manger(a: NourritureCarnivore) 
}

trait Herbivore <: Animal {
 def manger(a: NourritureHerbivore)
}

L'écriture trait Carnivore <: Animal signifie que la classe Animal et ses classes filles sont les seuls à pouvoir hériter de ce trait (on dit aussi mixer le trait).
Par contre maintenant il va falloir redéfinir la methode manger dans chacune des classes filles d'animal :

class Chien(nom: String) extends Animal(nom) with Carnivore {
  def manger(nourriture: NourritureCarnivore) = { aFaim = false }
  //...
}

object Spot extends Animal("Spot", true) with Carnivore with Herbivore {
  def manger(nourriture: NourritureCarnivore) = { aFaim = false }
  def manger(nourriture: NourritureHerbivore) = { aFaim = false }
  //...
}
Un problème final : la visibilité

Un dernier problème qui n'a pas été mentionné par Jojo est que l'utilisateur peut accéder aux attributs de la classe Animal et ses classes filles à la volée. Rien n'empêche un utilisateur maladroit de changer la valeur de "aFaim" ou "dresse", ou méme d'appeler faireLeShow à tout moment. On a rencontré ce problème dans le chapitre précédent et on l'a surmonté avec l'utilisation du mot clé private qui change la visibilité du champ de publique à privé. Le seul bémol ici est que, dès que les champs seront déclarés comme privés, Jojo aussi sera incapable d'acceder à faireLeShow et dresser. On va donc utiliser une petite fonctionnalité que nous offre Scala : soit un champ "c" d'une classe définie dans un package "monPackage". En ajoutant private[monPackage] devant la déclaration du champs, "c" sera publique uniquement dans monPackages (et les packages englobés par celui-ci) et privée ailleurs. C'est exactement ce qui nous faut, n'est-ce pas ?

package monPackage {
  class MaClasse {
    def m1 = {} // méthode visible partout
    private def m2 = {} // méthode privée partout
    private[monPackage] m3 = {} // méthode visible uniquement dans le bloc de monPackage
  }
}

On peut donc appliquer ce principe à notre programme, il suffit de regrouper toutes les classes dans un package :

package cirque {

  class Nourriture
  class NourritureCarnivore extends Nourriture
  class NourritureHerbivore extends Nourriture

  abstract class Animal(val nom: String, protected var dresse: Boolean = false) {
    protected var aFaim = false

    private [cirque] def faireLeShow = {
      println("Je ne fais rien !!")
      aFaim = true
    }

    private [cirque] def dresser = {
      dresse = true
      println("J'ai été dressé !!")
    }

    def afficher = println("Je suis un animal, je m'appelle " + nom)
      
  }
  trait Carnivore <: Animal{
    def manger(nourriture: NourritureCarnivore)
  }

  trait Herbivore <: Animal {
    def manger(nourriture: NourritureHerbivore)
  }

  class Chien(n: String) extends Animal(n) with Carnivore{
    def seDeplacer = println("Je marche")
    override def faireLeShow =
      if(dresse) {
        println("Je danse !!")
        aFaim = true
      }
    else super.faireLeShow

    def manger(nourriture: NourritureCarnivore) = { aFaim = false }
  }
  //Serpent, Poisson, Spot ..
  object Jojo {
    def dresser(a: Animal) = a.dresser
    def faireManger(animal: Carnivore) = 0
  }
}

Notez qu'ici les champs "nom", "dresse", et "aFaim" sont déclarés protected. Un champ protected est privé partout sauf dans les classes filles. En fait, d'une part, ça ne sert à rien de les déclarer comme private[cirque] puisque Jojo n'a pas besoin (et ne doit pas) les modifier directement, et, d'autre part, si on les déclare comme private ils ne seront pas visibles dans les classes filles, ce qui illogique (une classe Chien où on ne peut pas accéder au nom de ce chien est ... bizarre).

Je pense que maintenant c'est bon, on peut envoyer le programme à Jojo et être sûr qu'il sera satisfait. :)

Hiérarchie de la bibliothèque standard

Avant de finir, je souhaite vous expliquer brièvement comment fonctionne la bibliothèque standard de Scala. Il faut savoir qu'on a utilisé l'héritage (implicitement) depuis la première classe qu'on a écrit dans ce tutoriel. Vous n'avez rien remarqué parce que c'est bien caché. En réalité, toute classe qu'on écrit hérite implicitement de la classe AnyRef définie dans le package scala. Cette classe hérite elle meme de la classe Any, classe mère de toutes les classes en Scala. Any possède deux classes filles :

  • AnyVal, de laquelle héritent les types dits "types valeurs" qu'on trouve parmi eux Int, Char, Bool et Double. Ce sont ces types qu'on a appelé "types VIP" dans les premiers chapitres. Ils ont la particularité qu'on ne peut dans aucun cas utiliser "new" ni appeler une méthode factory pour les créer, on peut juste utiliser des valeurs prédéfinies comme true, 0, 1.2 ou encore 'A' ;

  • AnyRef, de laquelle héritent toutes les autres classes (et singletons) de l'API standard, ainsi que toutes les classes qu'on crée. Les types fils de AnyRef sont appelés types références.

L'utilité de tout cette hiérarchie est de pouvoir créer des méthodes qui sont disponibles pour toutes les classes (c'est un des avantages de l'héritage, comme on a vu dans ce chapitre). Je vais ici vous parler exclusivement de deux de ces méthodes : toString et equals.

La méthode toString

Cette méthode ne prend aucun argument et retourne une chaine. Elle est utile lorsqu'on désire convertir un objet en un String. Dans sa définition dans AnyRef, elle retourne la référence de l'objet. C'est pour cette raison que, lorsqu'on a voulu afficher les instances de Zero on a eu un affichage des références ; la méthode println ne peut afficher qu'un String, mais lorsqu'on on lui passe un objet d'un autre type, elle appelle implicitement toString pour obtenir une chaine. On peut donc remplacer toutes nos méthodes "afficher" par une redéfinition de toString.

La méthode equals

Lorsqu'on compare deux objets avec l'opérateur '==', c'est en réalité la méthode "equals" qui est appelée. Elle prend un objet de type Any en paramètre et retourne un Boolean. La redéfinition de "equals" n'est pas évidente à faire. Des analyses faites sur des codes de gens professionnels en Java, C++ et C# ont montré que 90% des développeurs sont incapables de redéfinir equals correctement. Pour cette raison on va consacrer tout un chapitre pour la redéfinition de equals plus tard dans ce tuto.

Voila un très long chapitre qui s'achève, et je pense qu'il sera le chapitre le plus volumineux de tout le tutoriel. Un grand nombre de notions a été introduit donc n'hésitez pas à relire quelques parties de temps en temps, afin de vous assurer que tout est clair. L'héritage et le polymorphisme ne sont pas faciles à expliquer, j'espère que j'ai pu les expliquer correctement. Si il y a quelques passages qui ne sont pas clairs pour les débutants, merci de le signaler dans les commentaires.

Exemple de certificat de réussite
Exemple de certificat de réussite