IV. Classes et méthodes▲
À propos de ce chapitre
Vous venez d'apprendre comment créer vos propres structures de données. Dans ce dernier chapitre, nous passerons à la vitesse suivante, en transformant ces structures en classes (Adresse, Personne et Carnet), et en leur ajoutant des méthodes.
Ce seront de véritables classes Ruby, au même titre que Integer, String, Array ou Hash.
Créer ses propres classes procure quelques avantages intéressants :
- Réutilisation du code : vous souvenez-vous du nombre de lignes qu'il a fallu écrire pour afficher le contenu du carnet d'adresses? N'aurait-il pas été plus joli d'écrire ce code quelque part, et d'ensuite appeler puts carnet chaque fois que nous désirions l'afficher ? C'est exactement ce que nous allons apprendre dans ce chapitre ;
- Abstraction des données : vous utilisez une classe en appelant ses méthodes. Le contenu de la classe en elle-même (l'implémentation des méthodes) vous est caché. Savez-vous comment la classe String est fabriquée? Probablement pas, mais vous êtes néanmoins capable de l'utiliser.
IV-A. Fonctions▲
IV-A-1. Qu'est-ce qu'une fonction ?▲
Une fonction est une méthode qui n'est pas associée à un objet en particulier. Vous avez déjà utilisé une fonction auparavant : puts. Remarquez bien la syntaxe :
puts "Salut!" # à la place de : un_objet.puts "Salut!"
IV-A-2. « Bonjour monde » avec une fonction▲
Voici une fonction toute simple :
def bonjour
puts "Bonjour monde!"
end
Maintenant, nous avons défini la fonction bonjour. Le code qu'elle contient sera exécuté à chaque appel de la fonction. Un exemple :
def bonjour
puts "Bonjour monde!"
end
bonjour
bonjour
Qui produira :
Bonjour monde!
Bonjour monde!
Comme vous pouvez dès à présent le constater, les fonctions peuvent servir à réutiliser du code facilement.
IV-A-3. Paramètres de fonction▲
Vous savez déjà qu'il est possible de passer des paramètres aux méthodes et fonctions. Mais vous ne savez pas encore comment faire! Voici la fonction bonjour légèrement améliorée :
def bonjour(nom)
puts "Bonjour " + nom + ", comment vas-tu"
end
bonjour("Laurent")
bonjour "Stéfanie"
Ce qui produira :
Bonjour Laurent, comment vas-tu?
Bonjour Stéfanie, comment vas-tu ?
IV-A-4. Afficher une adresse▲
Écrivons maintenant une fonction un peu plus utile. Souvenez-vous des structures d'adresses du chapitre précédent :
# Adresse de Nicolas
adresse_de_nicolas = {
"rue" =>"Rue du port, 32",
"code postal" =>"56000",
"ville" =>"Vannes",
"pays" =>"France"
}
# Adresse de François
adresse_de_francois = {
"rue" =>"Avenue de la tranchée, 14",
"code postal" =>"1000",
"ville" =>"Bruxelles",
"pays" =>"Belgique"
}
Voici le code d'une fonction qui permet de les afficher à l'écran :
def affiche_adresse(adresse)
code_postal = adresse["code postal"]
ville = adresse["ville"]
puts " " + adresse["rue"]
puts " " + code_postal + ", " + ville
puts " " + adresse["pays"]
end
Maintenant, nous pouvons facilement afficher des adresses :
puts "Nicolas:"
affiche_adresse(adresse_de_nicolas)
puts "François:"
affiche_adresse(adresse_de_nicolas)
Ce qui produira à l'écran :
Nicolas:
Rue du port, 32
56000, Vannes
France
François:
Rue du port, 32
56000, Vannes
France
IV-B. Classes et méthodes▲
Nous sommes maintenant prêts pour créer notre propre classe Adresse. Commençons simplement par une classe qui ne contient que le champ « rue ». Voici un exemple :
1
class Adresse
2
def initialize(rue)
3
@rue = rue
end
end
Explication
- 1 Le mot clef class définit une classe. On associe une méthode à une classe simplement en définissant la méthode à l'intérieur de la classe.
- 2 La méthode initialize permet de construire la classe. Il s'agit en fait du constructeur de la classe. Chaque classe doit contenir une méthode initialize!
- 3 @rue est une variable de classe. Ça ressemble un peu à une clef de hachage. Le symbole @ permet de distinguer les variables d'un objet. Chaque fois que vous allez instancier un objet de la classe Adresse, cet objet contiendra une variable @rue.
Utilisons maintenant cette classe pour créer un objet :
adresse = Adresse.new("Rue de la Renaissance, 49")
Et voilà le travail. adresse est dès à présent un objet provenant de la classe Adresse.
IV-B-1. Lire les données d'un objet▲
Comment s'y prendre pour obtenir la rue de notre objet adresse? Nous pouvons par exemple écrire une méthode qui renvoie les données :
class Adresse
def initialize(rue)
@rue = rue
end
# Renvoie simplement @rue
def rue
@rue
end
end
Maintenant, la méthode Adresse#rue vous permet d'obtenir la rue d'une adresse. Dans IRB :
>>adresse.rue
=>"Rue de la Renaissance, 49"
Une propriété d'un objet, visible de l'extérieur, est appelée attribut. Dans ce cas, rue est un attribut. Plus spécialement, un attribut en lecture, car il permet de lire la valeur d'une des propriétés de la classe.
Comme ce genre d'attribut se retrouve assez souvent, Ruby nous offre un raccourci : le mot clef attr_reader :
class Adresse
attr_reader :rue
def initialize(rue)
@rue = rue
end
end
IV-B-2. Modifier les données d'un objet▲
Évidemment, il est également possible de modifier une propriété d'un objet. Par exemple, en rajoutant une méthode dans la classe :
class Adresse
attr_reader :rue
def initialize(rue)
@rue = rue
end
def rue=(une_rue)
@rue = une_rue
end
end
Et voici comment utiliser cette nouvelle méthode :
adresse.rue = "Une autre adresse"
Remarquez que Ruby accepte des espaces entre rue et = (dans l'affectation). Par contre, il ne faut pas mettre d'espaces dans la définition de la méthode.
Maintenant que nous savons comment modifier les données de notre classe, nous n'avons plus besoin d'initialiser la rue dans le constructeur. Nous pouvons donc simplifier la méthode initialize :
class Adresse
attr_reader :rue
def initialize
@rue = ""
end
def rue=(une_rue)
@rue = une_rue
end
end
adresse = Adresse.new
adresse.rue = "Rue de la Renaissance, 49"
Cette petite modification rendra le code plus simple par la suite, lorsque nous rajouterons les autres données (le code postal, la ville, le pays).
Maintenant, rue est également un attribut en écriture, car via la méthode Adresse#rue=, il nous est possible de modifier sa valeur. Comme pour la lecture, il s'agit d'une opération courante, et Ruby nous permet d'utiliser le mot clef attr_writer :
class Adresse
attr_reader :rue
attr_writer :rue
def initialize
@rue = ""
end
end
IV-B-3. Accéder à des données▲
Fort souvent, vous aurez besoin d'attributs qui fonctionneront à la fois en lecture et en écriture. Comme vous vous en doutez peut-être, Ruby possède un mot clef qui regroupe ces deux états à la fois : attr_accessor :
class Adresse
attr_accessor :rue
def initialize
@rue = ""
end
end
Il est maintenant temps de définir entièrement la structure adresse de notre carnet sous la forme d'une classe. Le code ci-dessous ne devrait pas vous poser de problème :
class Adresse
attr_accessor :rue, :code_postal, :ville, :pays
def initialize
@rue = @code_postal = @ville = @pays = ""
end
end
Notez que attr_accessor accepte plusieurs arguments.
IV-C. Plus de classes▲
IV-C-1. Une classe Personne▲
Créons maintenant une classe Personne. Une personne doit avoir un prénom, un nom de famille, une adresse e-mail, un numéro de téléphone, et une adresse physique (celle que nous venons d'implémenter à la section précédente) :
class Personne
attr_accessor :prenom, :nom, :email, :telephone, :adresse
def initialize
@prenom = @nom = @email = @telephone = ""
@adresse = Adresse.new
end
end
La seule chose qui pourrait vous surprendre dans cet exemple, c'est la ligne @adresse = Adresse.new. La variable @adresse n'est pas une chaine de caractères comme les autres variables, mais un objet issu de la classe Adresse.
Il est maintenant possible de créer une personne :
# Adresse:
adresse_de_nicolas = Adresse.new
adresse_de_nicolas.rue = "Rue du port, 32"
adresse_de_nicolas.code_postal = "56000"
adresse_de_nicolas.ville = "Vannes"
# Personne:
nicolas = Personne.new
nicolas.prenom = "Nicolas"
nicolas.nom = "Rocher"
nicolas.email = "nicolas.rocher@free.fr"
nicolas.adresse = adresse_de_nicolas
Notez que nous n'avons ni affecté de pays, ni de numéro de téléphone à Nicolas. Comme par défaut, toutes les valeurs sont vides (c'est-à-dire « », nous pouvons créer des objets Adresse et Personne avec des informations incomplètes. Et nous pouvons toujours appeler nicolas.telephone sans aucun problème, Ruby nous renverra « » en retour. Essayez dans IRB !
Et si nous ajoutions une méthode dans la classe Personne, qui nous renverrait le nom complet de la personne représentée? Très facile :
class Personne
attr_accessor :prenom, :nom, :email, :telephone, :adresse
def initialize
@prenom = @nom = @email = @telephone = ""
@adresse = Adresse.new
end
def nom_complet
@prenom + " " + @nom
end
end
# ...
puts nicolas.nom_complet
Ce qui affichera « Nicolas Rocher ».
IV-C-2. Afficher le contenu d'une classe▲
Ne serait-il pas magnifique si nous pouvions simplement taper puts adresse_de_nicolas? Ruby est capable d'exaucer ce rêve.
La fonction puts travaille de la façon suivante : elle essaye d'appeler la méthode to_s sur l'objet qu'on lui donne, et elle affiche le résultat sur l'écran. Vous vous souvenez de Integer#to_s et de ses amis?
La seule chose à faire, c'est de définir une méthode Adresse#to_s :
class Adresse
attr_accessor :rue, :code_postal, :ville, :pays
def initialize
@rue = @code_postal = @ville = @pays = ""
end
def to_s
" " + @rue + "\n" + \
" " + @code_postal + ", " + @ville + "\n" \
" " + @pays
end
end
Le caractère \n représente une nouvelle ligne. Il s'agit un fait du caractère que votre clavier envoie lorsque vous pressez la touche Enter.
Nous pouvons maintenant taper ceci :
adresse = Adresse.new
adresse.rue = "Rue du port, 32"
adresse.code_postal = "56000"
adresse.ville = "Vannes"
adresse.pays = "France"
puts adresse
Ce qui affichera :
Rue du port, 32
56000, Vannes
France
IV-C-3. Quelques exercices▲
Écrivez une méthode Personne#to_s, qui devra afficher le nom complet de la personne, son adresse e-mail, son numéro de téléphone, et son adresse physique.
IV-D. Implémentation du carnet d'adresses▲
Maintenant que nous avons les classes Adresse et Personne, il ne nous reste plus qu'à créer la classe Carnet.
IV-D-1. Première étape▲
Notre carnet d'adresses doit contenir un tableau, qui contient tous nos contacts. Nous n'utiliserons pas attr_accessor parce que nous ne voulons pas qu'on puisse accéder directement au tableau. Nous allons donc écrire nos propres méthodes pour travailler sur le tableau interne.
Voici à quoi peut ressembler le code de notre classe Carnet :
class Carnet
def initialize
# Initialise le tableau. Meme chose que ``Array.new''.
@personnes = []
end
end
C'était plutôt facile. Rajoutons maintenant deux méthodes d'accès : Carnet#ajoute et Carnet#retire :
class Carnet
def initialize
@personnes = []
end
def ajoute(personne)
#1
@personnes.push(personne)
end
def retire(personne)
#2
@personnes.delete(personne)
end
end
Explication du code
- 1 : La méthode Array#push permet d'ajouter un objet dans un tableau. L'objet sera empilé sur les objets existants, un peu comme si vous rajoutiez une assiette sur une pile d'assiettes.
- 2 : La méthode Array#delete permet de supprimer un objet d'un tableau. Si le tableau contient plusieurs objets identiques, ils seront tous supprimés.
Pour mieux comprendre le fonctionnement de Array#delete, essayez ceci dans IRB :
>>a = [ 1, 3, 3, 3, 3, 5]
=>[1, 3, 3, 3, 3, 5]
>>a.delete(3)
=>3
>>a
=>[1, 5]
IV-D-2. Tri automatique▲
Nous allons maintenant rajouter une super fonctionnalité dans notre Carnet : un tri automatique. Par exemple, imaginez le code suivant :
carnet = Carnet.new
carnet.ajoute nicolas
carnet.ajoute francois
carnet.ajoute marina
Le carnet classera automatiquement les personnes à chaque ajout. Cette fonctionnalité va rendre notre classe Carnet bien plus intéressante qu'un simple tableau.
IV-D-2-a. Comment trier un tableau?▲
Nous voulons que le tableau contenant les contacts soit trié par ordre alphabétique, en se basant sur le nom complet de la personne (prénom et nom de famille). Dans la section précédente, nous avons écrit ceci :
# ``carnet'' est un tableau ici
carnet.sort do |personne_a, personne_b|
personne_a["prénom"] <=>personne_b["prénom"]
end
Adaptons ce code pour notre classe Carnet :
@personnes.sort do |a, b|
a.prenom <=>b.prenom
end
Si vous avez effectué les exercices du chapitre précédent, vous devriez déjà savoir comment trier les personnes en se basant sur leur nom complet. Voici une façon de le faire :
@personnes.sort do |a, b|
if a.prenom == b.prenom
a.nom <=>b.nom
else
a.prenom <=>b.prenom
end
end
Si les prénoms sont les mêmes, nous comparons les noms de famille. Sinon, on compare les prénoms.
IV-D-2-b. Simplification▲
Le principe fondamental de la simplification est de diviser le problème en petites parties. Nous pouvons déplacer le bloc de code dans une méthode :
def par_nom(a, b)
if a.prenom == b.prenom
a.nom <=>b.nom
else
a.prenom <=>b.prenom
end
end
Maintenant, nous pouvons écrire :
@personnes.sort do |a, b| par_nom(a, b) end
Ce qui est beaucoup plus simple à lire.
Il est possible de définir avec Ruby des blocs de code en utilisant deux syntaxes différentes :
@personnes.sort do |a, b|
# ...
end
@personnes.sort { |a, b|
# ...
}
Ces deux notations veulent dire exactement la même chose. La différence est que do … end est plus lisible, et que { … } est plus court.
Nous pouvons écrire le tri de notre tableau de cette façon :
@personnes.sort { |a, b| par_nom(a, b) }
Vous pouvez littéralement lire « tri de personnes par le nom ». C'est du code très lisible. Voici une suggestion :
- utilisez la notation { … } quand il est possible de faire tenir l'expression sur une seule ligne ;
- sinon, utilisez do … end.
IV-D-2-c. Finalement▲
Il est maintenant temps de déplacer ce code dans notre classe Carnet, et d'implémenter notre tri automatique :
class Carnet
def initialize
@personnes = []
end
def ajoute(personne)
@personnes.push(personne)
@personnes = @personnes.sort { |a, b| par_nom(a, b) }
end
def retire(personne)
@personnes.delete(personne)
end
def par_nom(a, b)
if a.prenom == b.prenom
a.nom <=>b.nom
else
a.prenom <=>b.prenom
end
end
end
Maintenant, à chaque fois que vous ajouterez une personne dans le carnet, ce dernier sera trié automatiquement!
IV-E. Écrire des itérateurs▲
Dans cette section, nous allons ajouter deux itérateurs dans la classe Carnet : Carnet#chaque_personne et Carnet#chaque_adresse. Au résultat, nous pourrons écrire ceci :
carnet.chaque_personne do |personne|
# ...
end
carnet.chaque_adresse do |adresse|
# ...
end
IV-E-1. Exécuter un bloc de code▲
Le mot clef yield permet d'appeler un bloc de code. Voici un exemple :
def deux_fois
yield
yield
end
deux_fois { puts "Vive Ruby!" }
Ce qui produira :
Vive Ruby!
Vive Ruby!
IV-E-2. Passage de paramètres▲
Vous pouvez utiliser yield exactement comme n'importe quelle autre méthode. Pour passer des arguments à un bloc de code, passez-les simplement à yield. Prenez cet exemple :
def noms
yield("Nicolas")
yield("François")
yield("Marina")
end
noms do |nom|
puts "Salut " + nom + ", comment vas-tu?"
end
Ce qui affichera à l'écran :
Salut Nicolas, comment vas-tu?
Salut François, comment vas-tu?
Salut Marina, comment vas-tu?
Vous pouvez passer autant de paramètres que vous voulez au bloc de code. Par exemple :
def noms
yield("Nicolas", "Rocher")
end
noms do |prenom, nom|
puts prenom + " " + nom
end
Ce qui donnera :
Nicolas Rocher
IV-E-3. Implémentation de Carnet#chaque_personne▲
Ce premier itérateur est le plus facile des deux à écrire : il suffit de parcourir chaque personne dans le tableau @personnes et d'appeler yield sur chaque élément :
class Carnet
# ...
def chaque_personne
@personnes.each { |p| yield(p) }
end
end
Et voilà !
IV-E-4. Implémentation de Carnet#chaque_adresse▲
Cet itérateur est quasi aussi simple à écrire que le premier. Au lieu de passer chaque personne au bloc de code, nous allons passer l'adresse de cette personne :
class Carnet
# ...
def chaque_adresse
@personnes.each { |p| yield(p.adresse) }
end
end
IV-E-5. Code complet de la classe Carnet▲
Juste pour tout mettre au clair, voici le code complet commenté de la classe Carnet. Il s'agit d'un morceau de code assez complexe, mais en découpant les tâches au fur et à mesure, il est beaucoup plus facile à maintenir :
class Carnet
#
# Méthodes fondamentales:
# initialize
# ajoute
# retire
#
def initialize
@personnes = []
end
def ajoute(personne)
@personnes.push(personne)
@personnes = @personnes.sort { |a, b| par_nom(a, b) }
end
def remove(personne)
@personnes.delete(personne)
end
#
# Iterateurs:
# chaque_personne
# chaque_adresse
#
def chaque_personne
@personnes.each { |p| yield p }
end
def chaque_adresse
@personnes.each { |p| yield p.adresse }
end
#
# Fonction de tri.
#
def par_nom(a, b)
if a.prenom == b.prenom
a.nom <=>b.nom
else
a.prenom <=>b.prenom
end
end
end
IV-F. Autres fonctionnalités▲
Avant de terminer, nous pouvons encore voir deux petites choses intéressantes concernant notre classe Carnet.
IV-F-1. Méthodes publiques et privées▲
Prenez Carnet#par_nom. Cette méthode est différente des autres sur un point très important : elle est utilisée à l'intérieur même de la classe. C'est ce qu'on appelle une méthode interne, ou privée.
Lorsque vous programmerez une méthode comme celle-ci, il est possible de la déclarer comme étant privée. Une méthode privée ne peut être appelée que par l'objet lui-même, et jamais par l'utilisateur. Une méthode normale (comme toutes celles que nous avons écrites) est appelée méthode publique.
Vous pouvez déclarer des méthodes en utilisant les mots clefs public et private. Quand vous ajoutez le mot clef private, toutes les méthodes définies à partir de là seront privées, jusqu'à ce que vous rajoutiez le mot clef public.
Par exemple :
class UneClasse
# Par défaut, les méthodes sont publiques
def methode1
# ...
end
private # Maintenant, les méthodes sont privées
def methode2
# ...
end
def methode3
# ...
end
public # Sauf celle-ci, qui sera publique
def methode4
# ...
end
end
Donc, dans notre carnet d'adresses, nous devons rajouter le mot clef private juste avant la définition de Carnet#par_nom :
class Carnet
#
# Méthodes fondamentales :
# initialize
# ajoute
# retire
#
def initialize
@personnes = []
end
# ...
private # Début des méthodes privées
#
# Fonction de tri.
#
def par_nom(a, b)
if a.prenom == b.prenom
a.nom <=>b.nom
else
a.prenom <=>b.prenom
end
end
end
IV-F-2. Réutilisation du code avec require▲
Nous avons passé beaucoup de temps à écrire les trois classes pour notre carnet d'adresses. Nous ne voulons pas copier et coller éternellement le code chaque fois que nous allons l'utiliser dans un programme. Fort heureusement, nous n'avons pas besoin de le faire.
Copiez le code des trois classes dans un fichier, et sauvez-le sous le nom carnet.rb. Maintenant, créez un nouveau fichier (dans le même répertoire) et tapez :
require 'carnet'
# Nicolas
adresse = Adresse.new
adresse.rue = "Rue du port, 32"
adresse.code_postal = "56000"
adresse.ville = "Vannes"
adresse.pays = "France"
puts "Nicolas:"
puts adresse
Sauvez-le sous le nom nicolas.rb par exemple. Maintenant, invoquez Ruby :
Vous l'aurez compris, le mot clef require permet de charger du code existant.
IV-G. Écrire de bons programmes▲
L'utilisation propice de fonctions, méthodes et classes permet de distinguer les programmeurs expérimentés. Vous trouverez dans cette dernière section quelques conseils qui vous aideront par la suite.
IV-G-1. Fonctions et méthodes▲
Les fonctions et les méthodes doivent toujours se charger d'une seule chose :
- si vous ne pouvez pas résumer en une ligne ce que fait votre fonction, elle est probablement trop compliquée ;
- si le code de votre fonction ne tient pas sur un seul écran, elle est probablement trop longue ;
- des études ont démontré qu'un être humain se souvient au maximum de 7 choses à la fois. Si votre fonction comporte plus de 6 variables, elle est probablement trop compliquée, et trop longue.
IV-G-2. Commentaires▲
Plus vous écrirez de programmes complexes, plus l'usage de commentaires se révélera important :
- les commentaires ne doivent pas nécessairement être longs. Ils doivent juste expliquer clairement l'objectif du code ;
- chaque fonction devrait avoir un commentaire qui stipule ce que fait la fonction. Exceptionnellement, vous pouvez omettre le commentaire si la fonction est si simple que son usage est évident ;
-
dans le cas du carnet d'adresses :
- les méthodes initialize, ajoute et retire n'ont pas besoin d'être commentées,
- dans le code de la classe, les méthodes chaque_personne et chaque_adresse ont un seul commentaire, qui stipule que ce sont des itérateurs. Une fois que le programmeur sait que ces méthodes sont des itérateurs, il devine plus facilement l'usage de ces méthodes,
- même chose pour la méthode par_nom, dont le commentaire explique au lecteur qu'il s'agit d'une fonction de tri ;
- les commentaires peuvent également être utilisés pour grouper ensemble une série de fonctions communes. Par exemple, dans le carnet d'adresses, nous avons utilisé des commentaires pour rassembler les « méthodes fondamentales », « itérateurs » et « fonctions de tri ».
Essayez toujours de diviser le code en petites parties, plus facilement maintenables.