L'orientation objet de Ruby
Jusqu’ici, nous avons vu des fonctionnalités classiques que tout langage de programmation offre, avec il est vrai quelques particularité, dans la syntaxe et aussi dans la philosophie sous-jacente (méthodes et fonctions, par exemple). L’orientation objet de Ruby est le point réellement intéressant.
Les classes
En Ruby, on définit une classe à l’aide du mot clef ’’class’’. La fin de la classe est délimitée par le mot clef ’’end’’. Voici comment définir une classe simple en Ruby :
class Ma_classe < classe_parent
def initialize
...
end
end
def ma_methode
...
end
L’héritage est possible, mais facultatif, à l’aide de l’opérateur plus petit que (<). Le nom de la classe doit-être une constante, c’est à dire commencer par une majuscule. Les méthodes de la classe sont définies à l’aide du mot clef ’’def’’. Les objets sont créés par la méthode ’’new’’ et cette dernière appelle la méthode ’’initialize’’ de la classe. Les arguments passés à ’’new’’ sont automatiquement transmis à la méthode ’’initialize’’. Donc, la meilleure solution pour initialiser un objet est d’utiliser cette méthode ’’initialize’’ et non ’’new’’. De plus la méthode ’’initialize’’ est automatiquement privée. En effet, il est possible de définir trois états de visibilité pour les méthodes : ’’public’’, ’’private’’ et ’’protected’’.
class Ma_classe
# Défini des accesseurs pour chaque variable
attr_reader :var1
attr_writer :var1
attr_accessor :var2
# On peut résumer ces 3 lignes par
attr_accessor :var1, :var2
# Variable de classe, similaire à un membre statique en C++
@@count = 0
# Méthode appelée par new et automatiquement privée
def initialize( arg )
@@count += 1
@var1 = arg
@var2 = ""
puts "Initialisation"
end
# Méthode publique par défaut
def ma_methode()
puts "Ma méthode"
end
# Toutes les méthodes suivantes seront privées
private
def ma_methode_privee()
puts "Ma méthode privée"
end
# Toutes les méthodes suivantes seront publiques
public
def var1=( var1 )
@var1 = var1
end
end
# Créer un nouvel objet à l'aide de la méthode new de clui-ci
obj = Ma_classe.new( "Hello" )
obj.ma_methode
# Utilisation des accesseurs
puts obj.var1
puts( obj.var1 = 6 ).to_s
obj.var2 = "Accessor"
puts "var2 = " + obj.var2
# Obtient l'id et le nom d'une classe
puts obj.id
puts obj.class
# Empêche toute modification de l'objet
obj.freeze
# Sinon on obtient un message d'erreur
begin
obj.var2 = 4
rescue TypeError => e
puts "Objet gelé"
end
# Erreur, méthode privée !
begin
obj.ma_methode_privee
rescue Exception => e
puts e.class.to_s + " caught"
end
Analysons ce code source :
attr_reader :var1
attr_writer :var1
Les instructions ’’attr_reader’’ et ’’attr_writer’’ déclarent des accesseurs et des mutateurs pour les membres spécifiés. Chaque membres doit-être précédé par un double points (:) et s’il y en a plusieurs, ils doivent-être séparés par une virgule. L’instruction attr_writer permet de définir implicitement des méthodes comme celle-ci, ce qui est une économie d’écriture non négligeable :
def var1=( var1 )
@var1 = var1
end
Ainsi, nous pourrons plus tard utiliser ce mutateur en faisant simplement précéder le nom de l’attribut par le nom de l’objet puis d’un point (.) :
obj.var1 = 6
Mais on peut également déclarer des accesseurs et mutateur en même temps à l’aide de l’instruction ’’attr_accessor’’ :
attr_accessor :var1, :var2
Si aucune méthode n’est définie en tant que mutateur alors Ruby affectera automatiquement la valeur de l’opérande droite à l’attribut.
@@count = 0
Cette instruction déclare et initialise une variable de classe grâce au préfixe double arobase (@@). Ce genre de variable est similaire à un membre statique en C++. Ainsi tous les objets de la même classe (et les classes dérivées également) la partageront. Cela peut se révéler très utile pour compter le nombre d’instances d’une classe ou pour économiser de la mémoire, par exemmple.
La méthode ’’class’’ retourne un objet de type ’’Class’’ qui décrit le type de l’objet. Ainsi on peut obtenir le nom de la classe ou encore ses classes de base.
Ensuite le programme appelle la méthode ’’freeze’’ de ’’obj’’. Elle permet de geler un objet pour que celui-ci ne puisse plus être modifé. Toute tentative de modification provoque une erreur qui peut-être intercepté à l’aide d’une exception, avec ’’rescue’’.
begin
obj.var2 = 4
rescue TypeError => e
puts "Objet gelé"
end
Il est également possible d’intercepter une exception lorsqu’on appelle une méthode privée. Ceci nous amène à voir les exceptions en Ruby.
Les exceptions
Voici la structure d’un code gérant les exceptions :
begin
... # Code succeptible de lancer une exception
rescue Type_exception => e then
... # Code éxécuté si une exception est capturée
else
.. # Code si aucune exception n'a été capturée
ensure
... # Code éxécuté dans tous les cas
end
Le code qui est succeptible de générer une exception doit-être placé dans une instruction ’’begin’’. Puis le code traîtant les exceptions est contenu dans le bloc ’’rescue’’. Si aucun type d’exception n’est indiqué, toutes les exceptions seront interceptées. Le ’’then’’ situé après le type d’exception est obligatoire s’il y a d’autres instructions avant la fin de la ligne. Enfin, les deux derniers bloc ’’else’’ et ’’ensure’’ sont facultatifs. Le code du ’’else’’ est éxécuté s’il n’y a pas eu d’exception. Quant au bloc ’’ensure’’, il sera interprété dans tous les cas. Pour lever une exception, il est nécessaire d’utiliser l’instruction ’’raise’’ suivit de l’exception. Maintenant, nous somme capable de comprendre le code source suivant :
#! /usr/bin/ruby
# Intercepte une division par 0
begin
puts 5 / 0
rescue ZeroDivisionError => e
puts e
end
# Intercepte 2 types d'exception et ferme le fichier
# s'il a pu être ouvert
begin
f = File.new( "fichier", "r" )
rescue Errno::ENOENT, Errno::EACCES => e
puts e
else
# Exécuté s'il n'y a pas eu d'exception
f.close
ensure
puts "Il ne reste plus de fichier ouvert"
end
# Lance une exception si val est inférieur à 10
def fct_inutile( val )
raise Exception.new( "Inférieur à 10" ) if val < 10
end
i = 0
debut = 5
# Utilisation de l'instruction retry
begin
debut.upto( 15 ) { |i|
puts i
fct_inutile( i )
}
rescue Exception => e
puts e
debut = 10
retry # Recommence l'éxécution du bloc begin
end
# Intercepte un appel d'exit
begin
exit( 0 )
rescue SystemExit => e
puts "L'appel de la méthode exit n'aura pas d'effet car l'exception " +
"SystemExit a été interceptée"
end
# Une exception personalisée
class MonException < Exception
attr_reader :time
def initialize
@time = Time.new
end
end
# Démonstration d'un bloc ensure
begin
raise MonException.new
rescue MonException => e
puts "MonException lancée le " + e.time.to_s
exit( 0 )
ensure
puts "Le bloc ensure est toujours éxécuté, même après l'appel de exit"
end
puts "Cette instruction ne sera pas éxécutée"
L’éxécution de ce script produit cette sortie :
divided by 0
No such file or directory - fichier
Il ne reste plus de fichier ouvert
5
Inférieur à 10
10
11
12
13
14
15
L’appel de la méthode ’’exit’’ n’aura pas d’effet car l’exception ’’SystemExit’’ a été interceptée : MonException lancée le Sat Feb 19 11:44:48 CET 2005. Le bloc ’’ensure’’ est toujours éxécuté, même après l’appel de ’’exit’’.
On remarquera que l’instruction ’’retry’’ a permis de recommencer l’éxécution du bloc ’’begin’’. Avant d’appeller ’’retry’’, on aura pris soin d’affecter la valeur 10 à la variable début afin d’éviter une boucle infinie. Cette instruction est certes pratique pour recommencer une action afin que le programme se déroule sans erreur, mais elle est en même temps dangereuse. En effet, il est important de réinitialiser les paramètres qui ont provoqués cette exception pour ne pas créer de boucle infine.
Pour lever une exception, il existe plusieurs syntaxes :
raise classe, message
raise objet
raise message
raise
Si aucune classe n’est spécifiéé, Ruby utilisera ’’RuntimeError’’ par défaut. Le simple appel de ’’raise’’ sans paramètres dans un bloc ’’rescue’’ relève l’exception courante.
Surcharge des opérateurs
Pour surcharger un opértareur, il suffit de définir une méthode ayant le même nom que l’opérateur. Ceci est valable pour tous les opérateurs surchargeables (voir le tableau des opérateurs) excepté le + et le – unaires. Dans ce cas on écrira + ou -.
Voici un code source qui surcharge l’opérateur égal (-) :
#! /usr/bin/ruby
class Personne
attr_accessor :nom
attr_accessor :prenom
def initialize
@nom = ""
@prenom = ""
end
def -( droite )
return false if self.class != droite.class
return false if @nom != droite.nom
return false if @prenom != droite.prenom
return true
end
def to_s
@nom + " " + @prenom
end
end
p1 = Personne.new
p2 = Personne.new
p3 = Personne.new
p1.nom = "Yukihiro"
p1.prenom = "Matsumoto"
p2.nom = "Yukihiro"
p2.prenom = "Matsumoto"
p3.nom = "Stroustrup"
p3.prenom = "Bjarne"
puts p1.to_s + " et " + p2.to_s + " est la même personne." if p1 - p2
puts p1.to_s + " et " + p3.to_s + " sont deux personnes différentes." if p1 != p3
Modules
Les modules sont comme des classes, mais il n’est pas possible de les instancier. Par contre, on peut y définir des méthodes et des variables. La définition d’un module se fait à l’aide de l’instruction ’’module’’ suivit par son nom. Evidemment le nom doit être une constante, donc le premier caractère sera une majuscule. Lorsque qu’on définit un module déjà existant, les nouvelles fonctionalités seront ajoutées au module existant. Pour définir une méthode dans un module, il faut faire précéder le nom de la méthode par celui du module suivit d’un point (.).
Module Nom_module
def Nom_module.nom_méthode
...
end
end
_Attention Lorsqu’une classe inclut plusieurs modules qui comportent les même membres, le comportement du code ne sera pas forcément celui attendu. Ruby utilisera le membre du premier module inclus._
Les modules offrent une possibilité intéressante qui se nomme les mixins. Cela permet d’inclure les méthodes d’un module dans une classe, et d’accéder à ce qu’on appel l’héritage multiple dans nombre d’autres langages orientés objet ; avec des fonctionnalités supplémentaire. Une classe recevant un mixin pourra également utiliser les attributs déclarés dans le module. Cette opération s’effectue très simplement à l’aide de l’instruction ’’include’’, suivit du nom du module à inclure.
class Ma_classe
include Mon_module
...
end
Ainsi, ’’Ma_classe’’ aura accès à toutes les méthodes de ’’Mon_module’’ et à ses attributs également.
Les mixins sont utiles lorsque plusieurs classes utilisent le même code. Imaginons que nous voulons créer une sorte de STL en Ruby. Nous allons écrire des classes Vecteur, Liste, Hachage etc… Ces classes auront certaines choses en communs, comme des itérateurs par exemple. Il nous permettent de parcourir un Vecteur de la même manière qu’une Liste, élément par élément. Evidemment, nous allons devoir coder des méthodes pour le tri, la recherche, etc… La première idée qui vient à l’esprit est de coder une version de ces méthodes pour chaque classe que nous avons écrite.
Mais ce n’est pas du tout la bonne approche, tout du moins en Ruby. En effet, en procédant ainsi, nous allons dupliquer un code similaire dans toutes ces classes. Cette pratique n’est pas bonne pour la maintenance du code, puisque chaque modification sur l’algorithme de tri devra être appliquée plusieurs fois. Ssi plus tard si nous décidons de créer un nouveau conteneur, on devra réécrire une méthode de tri, de recherche etc… ce qui rajoute du code et donc impose de refaire des tests, et ainsi de suite. Dans ce cas là, les mixins se révèlent particulièrement appréciables, puisqu’ils permettent d’utiliser la même méthode sur tous les conteneurs à partir d’un code générique pour chaque algorithme, tout en autorisant d’éventuelles surcharges pour adapter finement les méthodes à des contextes différents.
# tri.rb
module Tri
def croissant
# ...
end
def decroissant
# ...
end
end
# conteneurs.rb
require 'tri'
module Recherche
def rechercher
# ...
end
end
class Liste
include Tri
# ...
end
class Vecteur
include Tri
# ...
end
L’instruction ’’require’’ permet d’ndiquer à Ruby d’inclure le fichier spécifié.
Voici un autre code source utilisant les mixins. Nous voulons transformer un objet sous forme xml. Il devra s’auto-documenter (nom des variables, nom de la classe etc…). Il serait trop lourd de faire une classe de base d’un objet sérialisable en xml. La meilleure solution est de créer un module qui, grâce à la reflexion, va pouvoir accéder à tous les attributs de l’objet à traduire sous forme xml. Cette solution est transparente pour les autres programmeur qui voudront utiliser notre module puisque la seule contrainte est d’inclure le module.
# Transforme un objet sous forme xml
module Obj2xml
def to_xml
buffer = "<ruby-object class-name='" + self.class.to_s + "'>\n"
# Récupère la liste des variables d'instances
var_list = instance_variables
# Met dans buffer le nom et la valeur de chaque variable d'instance
var_list.each { |var_name|
buffer += "<attribute name='" + var_name + "' value='" +
instance_variable_get( var_name ) + "'/>\n"
}
buffer += "</ruby-object>"
end
end
# Classe de test
class Song
include Obj2xml
attr_accessor :title
attr_accessor :author
attr_accessor :album
end
s = Song.new
s.author = "Bob Marley"
s.title = "She's gone"
s.album = "Kaya"
puts s.to_xml
Et voici la sortie du script :
Ensuite, il sera très facile de reconstruire cet objet à l’aide de ce document xml.
<< Syntaxe et originalités | Le kit du programmeur >>


