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 !

Les méthodes magiques

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

Lorsque j'ai commencé à écrire ce tutoriel, beaucoup de personnes m'ont déconseillé de commencer par la POO, sous prétexte que "ce n'est pas fait pour des débutants". J'avoue que je n'ai pas trouvé cette tâche facile au début, mais Scala m'a aidé avec sa syntaxe très flexible. Le concept orienté objet repose sur des notions théoriques, or, la théorie n'est pas ce que les gens aiment, surtout pour quelqu'un qui n'a jamais programmé. J'ai hésité pendant un certain temps entre introduire la POO dès le début tout en laissant la théorie pour plus tard, ou commencer par présenter un style impératif (des boucles et des if en console). Vous savez surement quelle manière j'ai choisi (sinon revenez lire les autres chapitres :p ). Pourquoi ? Parce que la plupart des personnes qui ont commencé par l'impératif ont du mal à "penser objet" et font des programmes OO de mauvaise qualité. Vous, par contre, les termes classe, objet, champ, méthode etc ... ne vous font plus peur. Vous savez créer des objets et les utiliser, et croyez moi ce n'est pas du gâteau ! Maintenant, l'heure de la vérité à sonné. Ce chapitre et celui d'après vous montreront la réelle puissance de la POO. N'ayez pas peur, il n y aura pas trop de théorie quand même et les exemples seront au rendez vous.

Mise en situation

On va continuer dans ce chapitre avec notre classe Zero (promis, on n'en reparlera plus dès le chapitre suivant :-° ). En effet, nous allons nous concentrer sur les défauts de cette classe et nous allons essayer de l'améliorer, tout en introduisant des concepts intéressants et fondamentaux de la POO. Bon, voilà la dite classe :

class Zero {
  var pseudo = "Anonyme"
  var age = 0
  var citation = "Aucune citation"
  def afficher = println("Nom: " + nom + " Age: " + age + " Citation:" + citation)
  def avancerAge = age += 1
  var msg = "Aucun Message"
  def afficherMessage = println(msg)
  def envoyerMessage(nouveauMsg: String, z: Zero) = {z.msg = nouveauMessage}
}

Nous allons tout d'abord nous concentrer sur instanciation de la classe. Voici un cas de figure :

val einstein = new Zero
einstein.pseudo = "Einstein++"
einstein.age = 21
einstein.citation = "J'adore Scala <3"

Afin de créer une instance "valide" de la classe Zero, il faut initialiser à la main les champs pseudo et age car un Zero qui a pour pseudo "Anonyme" et pour age 0 ne veut rien dire. Le problème ici est qu'on risque facilement d'oublier d'initialiser les champs, surtout lorsqu'on a un plus grand nombre de champs (6, 8, 20 ...).

Et si je te dis que j'ai une bonne mémoire et que je peux bien me rappeler de tous les champs de mes classes ??

Bon ça résout seulement une partie du problème, puisque l'utilisateur d'une classe n'est pas toujours son créateur. On a utilisé tout au long de ce tutoriel les classes Int et String mais on n'a aucune idée sur leur implémentation (et on n'a pas besoin de le savoir pour les utiliser).
Un deuxième problème : les valeurs par défaut. Quel valeur par défaut doit on mettre à age ? 0 ou -1 ? Mais dans les deux cas, l'expression new Zero va créer un Zéro ayant un age ... pas très réaliste. De même pour le pseudo. Pourquoi "Anonyme" et pas "Pas de nom" ou encore une chaine vide ? Le problème ne se pose pas pour le champ citation puisque c'est tout a fait légitime de ne pas avoir une citation.
Comme vous voyez, c'est assez confus, et il n y a pas vraiment de réponse convaincante à ces questions.
Un troisième problème, on peut changer la valeur du champ pseudo à tout moment, chose qui ne devrait pas être permise. Donc le pseudo devrait être plutôt immuable (val), mais hélas, si on l'initialise avec "Anonyme", notre pauvre restera anonyme à vie. :(

Les méthodes factory

L'idée ici est de créer une méthode, en dehors de la classe, qui se charge de :

  • la création d'une instance de la classe;

  • l'initialisation des champs de l'objet avec les valeurs passées à la méthode en tant que arguments;

  • le renvoi de l'objet créé comme valeur de retour de la méthode.

Vous n'avez rien compris ? Examinez alors ce code :

object Createur {
  def creerZero(nom: String, age: Int, citation: String = "Aucune citation") = { // l'argument citation est facultatif, on lui donne donc une valeur par défaut
    val resultat = new Zero
    resultat.nom = nom
    resultat.age = age
    resultat.citation = citation
    resultat // ou return resultat
  }
}

Ici Createur est un singleton qui possède une unique méthode creerZero, qui est la méthode Factory responsable de l'instanciation et de l'initialisation des objets de type Zero. Maintenant, on peut faire tout simplement :

val einstein = Createur.creerZero("Einstein++", 21, "J'adore Scala <3")
val mateo = Createur.creerZero("M@teo21", 30)

L'avantage de la méthode factory, en plus de la réduction du nombre de lignes de code, est qu'on est certain de ne pas oublier de champs et de les initialiser correctement. Un petit bémol: l'utilisateur reste capable d'utiliser new et d'initialiser l'objet à la main, on verra comment remédier à ça plus loin dans ce chapitre.

Les factory en Scala, encore plus puissants

Ce qu'on vient de voir est applicable dans tout langage objet, mais Scala propose quelques fonctionnalités qui simplifient l'utilisation des méthodes factory.
D'abord, on va renommer notre singleton Createur en Zero.

Ici, il s'agit d'une classe et d'un singleton, il n y aura donc aucun conflit. Le singleton est appelé, dans ce cas, le "compagnon" de la classe Zero (il a quelques propriétés particulières qu'on verra plus tard).
Donc maintenant on peut faire :

val einstein = Zero.creerZero("Einstein++", 21)

C'est déjà pas mal, mais on peut encore améliorer la méthode.
La deuxième modification qu'on va effectuer consiste à renommer creerZero en "apply". Pourquoi utiliser un nom aussi moche (et en anglais) ? Parce que Scala nous permet d'omettre le nom de la méthode lorsque celui-ci est apply. o_O
Pas de panique, c'est juste qu'au lieu d'écrire o.apply(argument) on peut faire o(arguments). On a déjà utilisé apply dans ce tuto, à vous de trouver ou et quand ! Bon je ne suis pas aussi masochiste que ça et je vais vous le dire quand même : pour accéder au premier caractère d'un String ch, on a fait ch(0), qui est en réalité un appel à une méthode apply définie dans la classe String (et non pas son compagnon). Donc ch(0) et ch.apply(0) ne diffèrent que du nombre de caractères écrits.
Revenons à notre méthode factory, une fois renommée en apply, on peut écrire :

val einstein = Zero("Einstein++", 21)

Génial ! C'est mille fois plus lisible et plus pratique que d'écrire Createur.creerZero("Einstein++", 21).

Les constructeurs

Un constructeur est une méthode appelée au moment de la création de l'objet. Comme son nom l'indique, son rôle consiste à "créer" (plutôt initialiser) l'objet. En utilisant un constructeur, on pourra initialiser les champs d'un objet au moment de la création, et on sera enfin capables d'initialiser proprement les champs immuables (vals).
En Scala, on a deux types de constructeurs qui ont des déclarations différentes: le constructeur principal et les constructeurs secondaires.

Constructeur principal

Le constructeur principal a une déclaration spéciale : la liste d'arguments est mise devant le nom de la classe et le corps entre les accolades, en dehors de toute autre méthode.

Huh ? o_O

J'ai bien dit "une déclaration spéciale". Vous comprendrez mieux avec un peu de code :

class <NomDeLaClasse>(/* Arguments du constructeur */ ) {
  // Déclaration des champs et méthodes 
  // corps du constructeur
}

Encore pas compris ? Appliquons ça à la classe Zero et vous verrez que c'est simple. D'abord, les arguments du constructeur (qui sont les mêmes que ceux de la méthode factory)

class Zero (nom: String, age: Int, citation: String = "Aucune citation") {

Et ensuite le corps du constructeur :

class Zero (_nom: String, _age: Int, _citation: String = "Aucune citation") {
  val nom = _nom
  var age = _age
  var citation = _citation
  // on peut faire ici tout ce qu'on veut (calcul, affectations, boucles, conditions ...)
  Zozor.direBonjour(this) // Rappel: le mot clé this réfère à l'instance actuelle de la classe (cf. le chapitre 3)
  // déclaration des méthodes, inchangée
}

J'ai renommé ici les arguments en _nom, _age et _citation pour éviter un conflit de noms.

Euh, on met où exactement les instructions du constructeur par défaut ? c'est un peu flou.

Toute expression mise dans le bloc principal de la classe et qui n'est pas définition d'une méthode est considérée comme partie du constructeur principal.
Maintenant, une fois qu'on a crée le constructeur, on peut créer les objets comme ceci :

val einstein = new Zero("Einstein++", 21)

Tout ça pour faire un truc identique à apply !! T'es idiot ou quoi ??

C'est loin d'être la même chose que le factory. Avec le constructeur, les champs ont été initialisés avec les bonnes valeurs dès le début, on a pu rendre nom immuable et en plus, il s'agit ici d'une construction d'objet et non pas d'un simple appel d'une méthode. Notez que maintenant on ne peut plus faire new Zero, ceci est dû au fait qu'on a supprimé le constructeur par défaut. On appelle constructeur par défaut le constructeur qui ne prend aucun argument. Lorsqu'on fait class A on déclare implicitement un constructeur qui ne prends pas d'argument (on peut faire class A() mais c'est inutile). La déclaration new Zero (ou new Zero()) essaye d'instancier la classe Zero en utilisant le constructeur par défaut, qui désormais n'existe plus, ce qui génère une erreur de compilation.

Avant de passer aux constructeurs auxiliaires, je vais vous montrer une fonctionnalité hyper pratique de Scala. Je ne sais pas si vous l'avez remarqué, mais avoir des variables nom et _nom, age et _age est un peu redondant. Dieu merci, on peut se débarrasser de ça facilement :

class Zero(val nom: String, var age: Int, var citation: String = "Aucune Citation") {
  Zozor.envoyerMessage(this)
  // déclaration des méthodes
}

Ce code est exactement équivalent a l'ancienne déclaration, sauf qu'il est plus compact et utilise moins de variables.

Les constructeurs auxiliaires

Il est parfois nécessaire, pour une raison quelconque, de créer d'autres constructeurs que le constructeur principal (pour donner plus de choix pour l'utilisateur, par exemple) : Ce sont les constructeurs secondaires, ils sont déclarés comme une méthode usuelle qui obéit aux règles suivantes :

  • son nom doit être this

  • pas de type de retour (même pas Unit)

  • la première expression doit appeler un autre constructeur (principal ou secondaire)

Imaginons qu'on veut créer un second constructeur qui inverse l'ordre de paramètres :

def this(_age: Int, _pseudo: String, _citation: String = "Aucune citation") = this(_nom, _age, _citation)

Et voila notre constructeur ! La première (et l'unique) instruction du constructeur auxiliaire est un appel au constructeur qui a pour signature (String, Int, String), qui n'est autre que le constructeur principal.
Ce qu'on a fait ici s'appelle la surcharge des constructeurs, et de façon analogue à la surcharge des méthodes, on ne peut pas avoir deux constructeurs ayant la même signature.
Appeler un constructeur auxiliaire est identique à l'appel du constructeur principal :

val einstein = new Zero(21, "Einstein++")

Une question: on n'a pas mis des var/vals devant les noms des variables du constructeur auxiliaires,
ça veut dire qu'on n'aura pas les champs si on crée un objet en utilisant ce constructeur ?

Bonne question. C'est pour cette raison (entre autres) qu'on a la règle "la première instruction doit appeler un autre constructeur". En fait, l'expression this(_pseudo, _age, _citation) à appelé le constructeur principal qui se chargera de la création des champs. Même si on avait plusieurs constructeurs et qu'on a appelé un constructeur auxiliaire, ce dernier doit appeler un autre constructeur et ainsi de suite jusqu'à finir par tomber sur le constructeur principal. ;) Et de ce fait, seul le constructeur principal à le droit de créer des champs avec val/var.

Encapsulation et visibilité

L'encapsulation est un concept fondamental en orienté objet. Avant d'expliquer ce que c'est l'encapsulation, il faut bien comprendre ce que c'est la POO.

Il était une fois, le paradigme procédural ...

Imaginez que quelqu'un vous demande d'écrire un programme (par exemple le mario du TP) sous la contrainte suivante : vous ne pouvez pas déclarer autres classes/singletons que le Main, vous avez uniquement le droit de créer des variables et des méthodes dans ce singleton.

Mais c'est fou ça ! Qui voudra faire un truc pareil ?

Croyez-le ou non, c'est ça le paradigme procédural, qui est utilisé dans un grand nombre de langages tel que C ou Pascal. :waw: Ce paradigme a régné sur le monde de la programmation durant de langues années jusqu'à ce qu'un jour, on découvre l'OO. L'un des points faibles du procédural est que toutes les méthodes (appelées fonctions et qui forment tout le programme) peuvent accéder à et modifier tous les champs (appelés variables globales) du programme. Dans la pratique, c'est une grande contrainte, puisque toute modification de l'un des composants du programme (fonctions ou variables globales) entrainera des modifications dans (presque) toutes les fonctions du programme (imaginer les conséquences pour un programme formé de centaines de fonctions)! La POO a pu trouver une solution en séparant les objets en deux parties : partie publique (interface) et partie privée. C'est le créateur de la classe qui définit ce qui est publique et ce qui est privé. L'utilisateur, quant à lui, ne peut utiliser que la partie publique de l'objet. Ainsi, le créateur de la classe peut changer la partie privée de l'objet et le code de l'utilisateur reste inchangé.

Les variables, méthodes et classes en Scala sont publiques par défaut. On doit utiliser le mot clé private pour rendre un champ privé. Rien ne vaut un petit exemple :

class A(private val x: Int) {
  private def foo = println("Bonjour !")
}

object Main {
  def main(args: Array[String]) {
    val a = new A(5)
    println(a.x) // Erreur de compilation, x est privé
    a.foo  // Erreur de compilation, f aussi est privé
  }

Vous voyez mieux les choses n'est ce pas ?
Dans la plupart des langages orientés objet (C++, Java, C# ...) on applique toujours la règle suivante : toutes les variables membres de la classe (par exemple nom, age et citation dans Zero) doivent être déclarées comme privées.

S'ils sont tous privées comment va-t-on accéder aux champs pour les modifier ou les afficher ?

C'est simple : on définit pour chaque variable "X" deux méthodes :
un accesseur : généralement nommé getX, ne prend rien en argument et retourne la variable X
un mutateur : généralement nommé setX, prend un seul argument X1 de même type que X et affectes la valeur X1 à X.
Voici un exemple :

class Personne(private val nom: String, private var age: Int) {
  def getNom = nom
  // pas de mutateur pour une variable immuable (val) !

  def getAge = age
  def setAge(age1: Int) = { age = age1 } // les accolades sont facultatives mais c'est plus lisible que setAge(age1: String) = age = age1
}

L'avantage de cette approche est qu'on peut faire des tests dans ces deux méthodes, notamment dans le mutateur (on peut par exemple vérifier que nom1 n'est pas vide avant d'affecter sa valeur à nom, ou de vérifier qu'une âge est positive). Le seul problème ici est que définir ces deux méthodes pour tous les champs est fastidieux et rend le code plus volumineux. En plus, écrire p.nom est plus simple que p.getNom, et ça fait 3 caractères de moins. :p
J'ai bien dit que c'est ce qu'on fait dans la plupart des langages, voila ce qu'on fait en Scala :

class Personne(private val _nom: String, private var _age: Int) { // vous comprendrez plus tard pourquoi ce changement de noms
  def nom = _nom
 
  def age = _age
  def age_=(age1: Int) = { if (age1 > 0) _age = age1 } 
}

Ce qui est nouveau ici est surtout le mutateur de age avec son nom bizarre. En fait, Scala nous offre un sucre syntaxique pour les méthodes qui finissent par _= et qui n'ont qu'un seul argument. Au lieu de faire o1.xx_=(o2) on peut écrire o1.xx = o2. Regardez ce que ça donne avec la classe Personne :

//dans le main
val p = new Personne("Alex", 12)
println(p.nom) //ici on appelle la méthode nom
println(p.age)  // appel à la méthode age
p.age = 13   // traduit en p.age_=(13)
p.age += 1 // traduit en p.age_=(p.age + 1)
p.age = -12 // ici l'affectation n'aura pas lieu et p.age conserve sa valeur précédente
println(p.age) // affiche 14

Du point de vue de l'utilisateur, age et nom sont des variables, alors qu'en réalité il s'agit d'appels à des méthodes. On gagne donc la lisibilité des variables (p.age += 1 est plus lisible que p.setAge(p.getAge + 1) ) et la sécurité des méthodes (l'opération p.age = -1 ne change pas la valeur de _age ici à cause du if dans age_= ).
Je vois déjà ceux qui ont utilisé auparavant d'autres langages objets tomber amoureux de Scala. :lol: Et ce n'est pas encore fini, je vous ai laissé la plus grande surprise pour la fin : toutes les variables membres en Scala sont privées par défaut (contrairement à ce que j'ai dit plus haut).

Mais ... mais comment est-ce possible ? Sans mettre private on a pu accéder aux champs sans problème ! Et puis, s'ils étaient toujours privés, à quoi sert alors d'ajouter private ? o_O

Calmez-vous, je vous vous expliquer tout. En réalité, toute variable membre XXX est privée, mais Scala ajoute dans la classe implicitement les deux méthodes suivantes :

def XXX = XXX
def XXX_=(XXX1: <type-de-XXX>) = { XXX = XXX1 }

Et oui ! Scala crée l'accesseur et les mutateurs "triviaux" pour nous (le mutateur n'est crée que lorsque XXX est une var). D'ailleurs c'est pour cette raison que j'ai changé dans l'exemple précédent les noms des variables membres en _nom et _age, puisque ce code ne compile pas :

class Personne(private val nom: String, private var age: Int) { 
  def nom = _nom
 
  def age = _age
  def age_=(age1: Int) = { if (age1 > 0) _age = age1 } 
}

Le problème dans cette déclaration est que Scala va générer automatiquement des méthodes nommées nom, age et age_= ayant les mêmes signatures que celles qu'on a défini manuellement. On va donc finir par avoir des méthodes déclarées deux fois, ce qui ne va pas plaire au pauvre compilateur.
Enfin, l'ajout du mot clé private avant la déclaration de la variable déclare le mutateur et l'accesseur générés par Scala comme privés, et nous empêche donc d'accéder au contenu de la variable.
En conclusion, pour voir à quel point Scala est agréable à utiliser, voici deux codes équivalent, sauf que l'un est écrit en Scala et l'autre en Java :

class Personne(val nom: String, var age: Int)
public class Personne {
    private final String nom;
    private int age;
    Personne(String _nom, int _age) { // constructeur, pas de notion de constructeur principal en Java (ni en C++ ni en C# ni en ...)
        nom = _nom;
        age = _age;
    }
    public String getNom() {
        return nom;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age1) {
        age = age1;
    }
}

Je pense que ce chapitre n'était pas si difficile, n'est ce pas mes chers Zéros ? :-°
Bon je vous assure en tout cas qu'il est beaucoup plus simple que le chapitre suivant. Je ne suis pas en train de vous effrayer mais je vous signale seulement que nous sommes entrain d'introduire des notions fondamentales en POO et qui posent problème généralement aux débutants. Mais puisque je suis un excellent professeur et vous êtes les étudiants les plus intelligents que j'ai jamais eu, je parie que vous n'allez avoir aucun problème de compréhension? ;)

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