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 !

Singletons et modularité

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

Après la pause que nous avons pris durant le TP, nous allons maintenant retourner à la programmation orientée objets, et apprendre une nouvelle notion : les singletons. Nous allons également nous débarrasser complètement de la console noire, et utiliser l'EDI Netbeans (il est temps de l'installer et de le configurer si ce n'est pas déjà fait).

Bonne lecture.

Singletons

La dernière fois que vous avez vu la classe Zéro, elle avait cette allure :

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 maintenant créer une classe Zozor pour représenter l'âne le plus intelligent au monde (c'est la mascotte de ce site, pour ceux qui ne le connaissent pas). Il sera capable d'afficher un message à l'écran et d'envoyer un message à un Zéro :

class Zozor {
  def afficher(msg: String) = println(msg)
  def envoyerMessage(nouveauMsg: String, z: Zero) = {z.msg = nouveauMsg}
}

Rien de bien compliqué. Il s'agit d'une classe simple possédant deux méthodes, simples aussi.
Le code vous parait correct ? Rien de bizarre ?

Oui c'est correct puisque lorsque je charge le fichier j'ai "defined class Zozor" Qu'est ce qui cloche donc ?

Oui le code est syntaxiquement correct mais faux dans sa logique. o_O
Observez cette utilisation de Zozor :

val zozor1 = new Zozor
val zozor2 = new Zozor

Alors, vous n'avez encore rien remarqué ?

Enfin ! ^^ Notre code ne génère pas d'erreurs mais il n'a aucun sens. Il n'existe qu'un et un seul Zozor sur terre, donc pouvoir créer plusieurs instances de la classe est absurde.

La solution consiste à remplacer la classe par un singleton. Un singleton est une classe qui n'a qu'une et une seule instance ayant le même nom que cette classe. La déclaration d'un singleton est semblable à celle d'une classe, on doit seulement remplacer le mot-clé class par object (qui signifie objet) :

object Zozor {
  def afficher(msg: String) = println(msg)
  def envoyerMessage(msg: String, z: Zero) = z.msg = msg
}

Ce qui s'est passé ici c'est qu'on a créé une classe qui n'a pas de nom (on dit que c'est une classe anonyme) et on l'a instancié en même temps. L'instance créée s'appelle Zozor et puisque la classe ne possède aucun nom, on ne peut plus créer d'autres instances. :magicien:

Puisqu'un singleton est un objet, on peut appeler ses champs directement. Voici un exemple :

Zozor.afficher("Bonjour les Zéros !!") !
val z: Zero = new Zero
Zozor.envoyerMessage(z, "Joyeux Anniversaire !")
z.afficherMessage

et la sortie console :

"Bonjour les Zéros !!"
defined module Zero
z: Zero = Zero@25fd26
"Joyeux Anniversaire !"

Tout est parfait. Zozor a affiché un message et il a ensuite souhaité un joyeux anniversaire au Zéro "z". Le "defined module" est l'équivalent du "defined class" pour les classes.
Choisir entre créer une classe ou un singleton n'est pas toujours évident, on doit parfois penser à ce qu'on va faire avec l'objet. Par exemple, pour une calculatrice deux cas de figure se présentent :

  • Si elle va juste servir pour des gros calculs dans un jeu alors un singleton fera l'affaire ;

  • Si on va créer un programme qui simule la vente des calculatrices, une classe sera obligatoire pour pouvoir en créer plusieurs instances.

Premier programme avec Netbeans

On va enfin pouvoir dire adieu à la console et commencer à utiliser Netbeans. Pour créer un projet Scala, suivez ces étapes :

  • Ouvrez votre IDE

  • Dans le menu File -> New Project -> Scala -> Next

  • Dans le champs de texte Project Name écrivez ScalaSDZ

  • Choisissez un dossier pour Project Location (votre projet sera enregistré dans ce dossier).

  • Laissez le reste tel qu'il est et appuyez sur Finish.

Vous devez vous trouvez avec ceci :

Image utilisateur

Je vous explique ce que sont les quatre zones :

  • zone (1) : c'est là où vous allez écrire votre code. Netbeans vous fournit déjà quelques lignes par défaut.

  • zone (2) : c'est la sortie, Netbeans y affichera tout ce qu'il veut vous dire. Cette zone comporte aussi la console.

  • zone (3) : vous trouverez ici l'arborescence de tout vos projets Netbeans.

  • zone (4) : Netbeans vous affiche ici l'arborescence des classes, variables et méthodes qui sont définis dans le fichier ouvert en (1) (Main.scala dans le screenshot). Cette zone est importante lors de la navigation dans de longs codes.

Code minimal

Le code affiché en (1) doit ressembler à ceci (en supprimant les commentaires):

package scalasdz

object Main {
  def main(args: Array[String]) : Unit = {
    println("Hello, world!")
  }
}

En supprimant le println, il reste le code minimal d'un programme Scala.

Commençons par ce que vous connaissez : on a un singleton Main qui contient une seule méthode "main", qui prend un tableau de chaines en argument (on verra les tableaux dans le chapitre sur les collections) et qui retourne un Unit. Tout programme Scala doit avoir une et une seule méthode "main" qui la même signature et le même type de retour que celle-ci. Cette méthode bien particulière est le point d'entrée au programme, c'est-à-dire que Scala commence par évaluer cette fonction.

Pour tester le programme, faites un clic droit sur le nom de votre projet (zone (3)) et choisissez "run" ou appuyez tout simplement sur <F6>. Dans la zone (2), un peu de blabla s'affiche pour vous dire que votre code est en train d'être compilé (transformé en bytecode Java) et puis vous aurez :

run:
Hello, world

"run" annonce le début d'exécution du programme et "Hello, world" est le résultat du println dans votre "main".

Contrairement à la console ordinaire, celle de Netbeans n'affiche ni les "resX" ni les "defined class", seule println est capable d'écrire dans la console (entre autres, on verra ça en détail un peu plus bas).

Vous pouvez créer n'importe quel nombre de classes/singletons par fichier mais si vos classes sont un peu longues il est préférable de mettre chacune dans son propre fichier. Pour créer une nouvelle classe faites un clic droit sur le dossier scalasdz (zone (3)) et puis choisissez new -> other -> Scala -> Scala class -> Next. Écrivez "Zero" comme nom de classe et appuyez sur "finish".

Copiez/collez le corps de la classe "Zero" dans Netbeans ainsi que le singleton Zozor. Ensuite, créez et utilisez quelques instances de "Zero" dans votre "main" :

def main(args: Array[String]): Unit = {
    val z = new Zero
    z.pseudo = "J-V"
    z.age = 34

    Zozor.envoyerMessage("Salut", z)
    z.afficher
    z.afficherMessage
    
    Zozor.afficher("Le nouveau LDZ sortira dans quelques jours :)")
  }

Appuyez sur F6, vous devez voir ceci dans la console (après le "run"):

Nom: J-V Age: 34 Citation:Aucune citation
Salut
Le nouveau LDZ sortira dans quelques jours :)

Votre programme entre au cœur de la méthode "main" et évalue toutes les expressions qui s'y trouvent dans l'ordre.

Modularité

Lorsqu'on a plusieurs fichiers .scala le projet devient rapidement encombrant et difficile à maintenir. Pour pouvoir se repérer facilement dans un grand nombre de classes on doit les répartir dans des dossiers appelés des "packages" (paquets). Ça vous rappelle quelque chose ? Regardez la première ligne du code généré par Netbeans :

package scalasdz

Netbeans n'a pas mis nos classes n'importe où, mais dans un dossier package nommé "scalasdz". Tout le code qu'on a écrit est mis dans ce package, en fait notre fichier .scala est équivalent à :

Un package peut en contenir d'autres (on dit qu'on imbrique les packages):

package p{

  package p1{

    package p11{}

    package p12{
      class C
    }
  }

  package p2{
    class C
  }
  class C
}

Le package p contient :

  • le package p1 qui contient:

    • le package vide p11;

    • le package p12 qui contient la classe C;

  • le package p2 qui contient:

    • le package p21 qui contient la classe C;

  • la classe C.

Ce qui est intéressant ici est qu'on a trois classes avec le même nom sans que Netbeans n'affiche aucune erreur. Pourquoi? Parce que, tout simplement les 3 versions de "C" n'ont pas le même nom. :-° Je vous explique : le nom du package fait partie du nom complet de la classe, donc dans le programme ci-dessus les classes s'intitulaient p.p1.p12.C, p.p2.p22.C et p.C. Il s'agit donc de 3 classes différentes \o/.

Les imports

Devoir répéter le nom complet de la classe lors de chaque utilisation devient rapidement fastidieux. Heureusement, on a une solution « magique » : utiliser un import. C'est simple, il suffit d'écrire le nom de classe précédé par le mot-clé import pour pouvoir utiliser le nom réduit de la classe.

import p.p1.p11.C
val x = new C

:magicien:

Portée des imports

Je vous ai parlé (brièvement) dans le deuxième chapitre de la portée des variables :

Citation

Les variables déclarées dans un bloc n'existent qu' à l'intérieur de celui-ci, on appelle ça la portée des variables.

Les imports ont exactement la même portée que les variables, ils ne sont valables que dans le bloc où ils ont étés créés.

Collisions de noms

Observez ce bout de code :

import p.p1.p11.C
import p.p2.C

Ce code ne compilera pas, Netbeans générera une erreur à la ligne X. En fait, puisqu'on a importé deux classes ayant le nom C, Scala ne peut pas deviner quelle classe il doit appeler entre sdz.C et sdz.package12.C. Pour remédier à ce problème, on doit renommer « temporairement » une des deux classes au moment de l'import. La syntaxe est la suivante :

import <nom-du-package>.{nomDeLaClasse => nouveauNom}
//exemple
import java.lang.{String => Chaine}

Appliquons cela à notre exemple :

import p.p1.p11.{C => C}
import p.p2.{C => C2}

Désormais, C1 est un alias pour p.p1;p11.C et C2 est un alias de p.p2.C (on aurait pu créer un alias pour une seule des deux classes).

Avant de passer à autre chose, j'ai une petite remarque à faire, ce code compile parfaitement :

import p.p1.p11.C
{
  import p.p2.C
}

Comment ça ? C'est identique au code de tout-à-l'heure !

Non, il y a une importante différence : dans le code ci-dessus les imports sont déclarés dans deux blocs différents (imbriqués). La classe C (p.p2.C) importée dans le bloc interne « cache » temporairement la classe C (p.p1.p11.C) importée dans le bloc externe. Donc le nom C design p.pp2.C à l'intérieur du deuxième bloc et p.p1.p11.C ailleurs. C'est perturbant et c'est une source d'erreurs, évitez donc ce genre de pratiques.

Revenons à la ligne "package sdz" générée par Netbeans. Notez ici qu'on a omit les accolade ; ces dernières seront ajoutées automatiquement de sorte qu'elles englobent tout le fichier (en d'autre terme, si "package sdz" est la 1ere ligne non vide du fichier, tout le code écrit dans ce fichier appartiendra au package sdz). Donc notre Main.scala est équivalent à :

package sdz {
  //tout le code sera mis dans le package sdz
}
Imports simultanés

On peut importer simultanément plusieurs classes se trouvant dans le même package:

import scala.collections.immutable.{List, Set} // on verra les classes List et Set ultérieurement

On peut aussi en renommer quelques uns :

import scala.collections.immutable.{List, Set => Ensemble} // on importe List et Set qu'on la renomme en Ensemble

Ou importer toutes les classes du package " _ " :

import scala.collections.immutable._

Le caractère " _ " est très important en Scala, il est appelé le joker et il a différentes significations selon le cas d'utilisation.

Encore des imports

Une dernière chose : on peut importer aussi les champs (variables membres et méthodes) d'un objet :

def main(Array[String] args) : Unit = {
  val x = "Bonjour"
  import x.concat
  val y = concat(" , les zéros") // équivalent à y = x.concat(" , les zéros")
  import y.{concat => yconcat}
  val z = yconcat(" !!")         // équivalent à z = y.concat(" !!")
}

C'est pratique parfois, mais n'en abusez pas car ça risque de devenir le bordel (en plus on perd l'aspect orienté objet du langage).

Le Singleton Console

Je vais dans cette sous-partie vous parlez d'un singleton très pratique : scala.Console. Comme son nom l'indique, il s'agit d'un singleton qui manipule la console. On s'en sert pour afficher du texte et pour lire des données entrées par l'utilisateur.

Affichage de texte et de variables

On va commencer par quelque chose que vous connaissez déjà, à savoir écrire des chaines en console. On va utiliser les méthodes Console.print et Console.println :

import scala.Console
Console.print('a') // affiche a
Console.print(3 + 5) // affiche 8
Console.println // n'affiche rien et fait un retour à la ligne, équivalent à Console.print('\n')
Console.println("Je vous aime les zéros <3") // affiche Je vous aime les zéros <3 et retourne à la ligne
Console.println(new Zero) // affiche une référence de la forme @ac34f

La méthode print se contente d'afficher le texte (ou l'objet) passé en paramètre, par contre println ajoute un retour à la ligne après l'affichage.

Quelle est la différence entre Console.println et println qu'on a utilisé avant ?

Il n'y a aucune différence, il s'agit de la même méthode. Vous savez déjà qu'on ne peut pas appeler une méthode sans objet, mis je vous ai dit que println est un peu spéciale. En réalité il n'y a rien de magique, c'est juste que parmi les imports par défauts il y a import scala.util.Console._. Donc toutes les méthodes de Console peuvent être utilisées directement, sans avoir à préciser le nom du singleton.
Notre code précédent est donc équivalent à :

print('a') 
print(3 + 5) 
println 
println("Je vous aime les zéros <3")
println(new Zero)
Lecture de données

Les programmes qu'on a écrit jusqu'à maintenant ont tous été statiques (sauf le TP), c'est à dire que même si on les exécute plusieurs fois, on aura toujours le même résultat. Dans le TP, on a ajouté un peu de dynamisme au programme grâce aux nombres aléatoires qu'on a généré avec le singleton Random (oui c'est un singleton, c'est pourquoi on n'avait pas besoin de faire new Random dans le TP). Console possède quelques méthodes qui nous permettront de rendre nos programmes encore plus dynamiques, on va en effet pouvoir interagir avec l'utilisateur du programme ! o_O N'ayez pas peur, vous verrez que c'est aussi simple que l'affichage du texte.
Commençons par tester cette ligne de code :

Console.readLine

Mettez cette ligne dans votre méthode main, exécutez le programme et observez !
Alors ?

C'est tout à fait normal, BUILD SUCCESFUL s'affiche lorsque le programme se termine, or le votre tourne encore, il a juste été bloqué. En fait, la méthode Console.readLine bloque la console jusqu'à ce que vous appuyez sur la touche <entrer>.

Alors ça sert juste à bloquer la console ??

Non, la méthode retourne quelque chose : Elle retourne la chaine que vous avez écrit avant d'appuyer sur <entrer> (ou "" si vous n'avez rien écrit). Allez-y testons cela :

val s = Console.readLine
println("Vous avez écrit: " + s)

Faites tourner le programme, écrivez n'importe quelle phrase, appuyer sur <entrer> et vous aurez l'affichage suivant :

Bonjour ma chère console 
Vous avez écrit: Bonjour ma chère console

Impeccable ! :soleil:
Ce qui s'est passé exactement est :

  • on a commencé par écrire une phrase dans la console;

  • la méthode Console.readLine à retourné un objet String qui contient la phrase écrite;

  • la variable s à été initialisée avec cet objet

  • on a affiché ("Vous avez écrit" +) s

Pour vérifier que vous avez bien compris le principe, écrivez un programme qui demande à l'utilisateur son nom et affiche ensuite un message de bienvenue.

println("Quel est votre nom ?");
val nom = readLine // ou Console.readLine
println("Vous êtes " + nom + " !!!");
println("Bienvenue sur le SDZ :D ")

Hyper simple ! Juste une remarque, on peut écrire simplement readLine puisque, comme j'ai dit plus haut, il y a un import scala.Console._ automatique ;)

Je veux tout lire !

La méthode readLine est hyper chouette, mais elle a le défaut qu'elle retourne toujours un String, même si on écrit dans la Console quelque chose comme 123 ou true. Heureusement, il y a des méthodes pour lire autre chose qu'une chaine de caractères. Ces méthodes sont : Console.readInt, Console.readDouble, Console.readChar et Console.readBoolean. Leurs comportement est identique à celui de readLine. Voici un exemple d'utilisation de quelques unes de ces méthodes :

println("Donner un entier: ");
val n = readInt
println("Donner un réel : ")
val x = readDouble
println("Vous avez entré " n + " et " + x)
val somme = n + x 
println("Leur somme est " + somme)

C'était un chapitre simple (je l'espère en tout cas). Si vous vous sentez en bonne forme, vous pouvez attaquer de suite le prochain chapitre qui, lui, est beaucoup plus intéressant que celui là.

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