Dans ce chapitre, nous allons voir ce qu’est un objet et comment l’utiliser. Nous allons également créer nos premiers objets et voir quelques généralités sur les objets. Puisque nous allons tester beaucoup de petits bouts de code, il peut être plus simple d’utiliser l’interpréteur.
Ruby et les objets
Qu’est-ce qu’un objet
Pour commencer, il nous faut voir ce qu’est un objet. On ne va pas faire compliqué, on va regarder le monde réel. Un objet, c’est un truc, une chose. Autour de nous, il y a pleins d’objets. Des livres, des ordinateurs, des feuilles, etc. On interagit avec eux, on leur fait faire des actions, on les manipule, on les fait interagir entre eux, etc.
Un objet a des propriétés (un livre a un titre, un auteur, etc.) et on peut le manipuler (on peut l’ouvrir, le fermer, le lire, le déchirer, etc.) et tout cela le caractérise. Si nous considérons un autre livre, c’est aussi un objet. C’est aussi un livre, mais c’est un objet différent.
Maintenant, nous voyons mieux à quoi correspond un objet de la vie réelle. Les objets dont nous allons parler sont à peu près les mêmes. Donc pour commencer, nous allons considérer qu’un objet est un truc, un bidule un machin.
Orientation objet de Ruby
En Ruby, un objet est souvent manipulé sous la forme d’une variable. Pour interagir avec lui, on effectue des opérations.
Ruby est un langage dit orienté objet. De nombreux langages de programmation sont orientés objets (Java, Python, etc.). Avec eux, on manipule les données grâce à des objets qu’on fait communiquer entre eux. On leur demande des informations, on les fait faire une action. On peut par exemple imaginer un objet avion
à qui l’on demande de voler.
Ruby est de plus un langage de programmation où tout est objet. Les entiers sont des objets, true
et false
sont des objets, les tableaux sont des objets, absolument tout ce que nous utilisons sont des objets. Pour manipuler ces objets, les opérations que nous utilisons sont les méthodes. Par exemple, nous pouvons parcourir tous les éléments d’un tableau avec la méthode each
ou obtenir le PGCD de deux nombres avec la méthode gcd
.
tab = [1, 2, 3] # Création d’un objet
tab.each { |e| print e + 1 } # Appel de méthode
nb = 36 # Création d’un objet
print nb.gcd(12) # Appel de méthode
Quand nous créons deux tableaux, ils ont les mêmes méthodes, des méthodes que les entiers ne possèdent pas forcément, que les hachages ne possèdent pas forcément et la réciproque est aussi vraie. En fait, on peut créer des objets sur des modèles préexistants. Nous verrons dans le chapitre suivant comment cela se passe, mais il nous faut garder cela en tête. Nous pouvons partir d’un modèle d’objet , d’un moule, pour créer de nouveaux objets.
Bien sûr, certains objets du monde réel ne seront pas implémentés comme on les voit dans la vraie vie. Déjà, cela dépend de notre vision de l’objet, et surtout parce que le but n’est pas de représenter la vraie vie mais de construire un programme robuste, maintenable, et qui fonctionne bien. En fait, certaines bonnes pratiques sont quasiment non naturelles. Néanmoins, elles permettent d’avoir du code bien organisé. Il faudra notamment regarder les principes SOLID, la loi de Déméter et certains patrons de conception. Pour avoir une idée rapide de ce que préconisent ces principes, nous pouvons lire cet article qui les présente de manière humoristique.
Création du premier objet
Pour créer un objet, nous allons tout simplement écrire ce code.
obj = Object.new
On appelle la méthode new
de l’objet Object
qui nous permet de créer un nouvel objet. Notons que Object
est un ce que nous avons appelé plus tôt une classe, c’est-à-dire qu’elle est un modèle sur lequel est construit notre objet obj
. obj
a été construit en utilisant le moule Object
. Cependant, obj
ne représente rien de spécifique, ce n’est pas un point, ni un personnage, ni un livre. C’est juste un objet banal, un truc.
Ce sera en effet à nous de spécifier le comportement de notre objet, pour qu’il devienne ce que nous voulons qu’il soit. Pour le moment, notre objet est tout à fait quelconque. On peut regarder s’il est égal à quelque chose d’autre, l’afficher, mais sinon, il n’y a rien de bien folichon à faire.
> print obj
#<Object:un_nombre>
> obj == 2
false
Le second résultat est tout ce qu’il y a de plus normal (ben oui, notre objet n’est pas égal à 2), mais le premier est quand même assez embêtant. On demande l’affichage de obj
et c’est ça qu’on obtient ! En fait, c’est quand même assez normal, on demande l’affichage de obj
, mais à quoi s’attend-on ? obj
c’est à peu près rien, que pourrait-on afficher ?
Construction d'un objet
Méthodes
Pour la suite, nous allons créer un objet message
. Commençons par le créer.
message = Object.new
Pour le moment, notre objet est très triste et à vrai-dire, on ne peut pas vraiment dire que c’est un message. Personnalisons-le. Pour commencer, donnons-lui quelques méthodes. Nous savons déjà définir des méthodes, nous pouvons définir une méthode pour notre objet de la même manière. Il nous suffit d’écrire le nom de l’objet suivi d’un point avant le nom de la méthode. Définissons quelques méthodes pour notre objet.
def message.author
'Karnaj'
end
def message.recipient
'Zeste de Savoir'
end
def message.content
'Quoi ? Un message ? Tout de suite ?'
end
def message.date
'27/12/4096'
end
On peut alors appeler les méthodes que l’on vient d’écrire.
puts "Message a été écrit par #{message.author} pour #{message.recipient}."
print "Contenu : « #{message.content} »."
Nous pouvons de même définir des méthodes avec paramètres comme nous l’avons vu, etc.
Avec les objets existants
En fait, ceci est également possible avec quasiment n’importe quel autre variable. Par exemple, on va définir une méthode renverse
pour notre variable str
.
str = 'abcd'
def str.invert
'dcba'
end
print str.invert
Il nous faut cependant faire attention. La méthode n’a été définie que pour la variable str
. Si nous l’essayons sur une autre variable, nous obtiendrons une erreur et ce même si l’autre variable est une chaîne de caractère.
other = 'abcd'
print other.invert # => NoMethodError: undefined method `invert`
En effet, dans ce code, nous avons créé une nouvelle chaîne de caractère. Mais lorsque nous définissons other
en lui affectant str
, cela fonctionne puisqu’il fait alors référence au même objet.
other
print other.invert
Bien sûr, la même règle s’applique aux objets que nous créons.
message_2 = message
message_2.author # => Karnaj
message_3 = Object.new
message_3.author # => NoMethodError
Conversions
Cependant, il reste un souci à effacer. Nous voudrions bien avoir le contenu du message lorsque nous utilisons print
. Pour cela, il nous faut savoir comment la méthode print
fonctionne. En fait, elle appelle la méthode to_s
de l’objet. Ainsi, elle obtient une chaîne de caractère à afficher. Il nous suffit donc de définir to_s
pour faire ce qu’on veut.
Notons que notre objet message
a déjà une méthode to_s
(grâce à la classe Object
à partir de laquelle il a été créé). Elle ne renvoie pas ce que l’on veut, nous allons la redéfinir.
def message.to_s
'Message de #{author} à destination de #{recipient}. Contenu secret.'
end
Maintenant, lorsque l’on affiche notre objet avec print
ou puts
, on a bien le résultat voulu. Comme quoi ce n’était pas compliqué comme problème.
En écrivant author
, nous appelons la méthode author
de l’objet courant (donc de message
). De manière générale, on peut appeler n’importe quelle méthode de l’objet courant de cette façon.
Dans le même genre, nous pouvons définir des méthodes pour convertir notre message dans un autre type. Nous pourrions par exemple faire une méthode convert_into_integer
, mais pour respecter les conventions de Ruby, nous allons garder les noms que nous connaissons déjà. Bien sûr, nous n’allons définir que les méthodes qui ont un sens (définir to_i
ou to_f
n’aurait pas beaucoup de sens).
def message.to_a
[author,
recipient,
content,
date]
end
Analyse d'un objet
Dans cette partie, nous allons surtout faire de la théorie. Nous allons pratiquer sur des choses qui nous seront utiles en théorie et qui sont toujours bonnes à savoir.
Inspecter un objet
Il est temps d’inspecter notre objet, de voir ce qu’il a dans les entrailles. Nous nous doutons bien que notre objet n’est pas vide, et qu’il est créé avec quelques méthodes. D’ailleurs, nous avons pu remarquer qu’il avait déjà une méthode to_s
(sinon, utiliser puts
avec notre message n’aurait pas fonctionné). En fait, notre objet a été créé avec quelques méthodes. Pour connaître les méthodes d’un objet, nous utilisons la méthode methods
qui nous renvoie le tableau des méthodes d’un objet.
print message.methods
Nous pouvons constater que les méthodes que nous avons créées sont aussi présentes, mais ce qui doit le plus nous marquer, c’est le nombre de méthodes déjà présentes. Il y en a déjà beaucoup.
Ici, nous allons nous intéresser à quelques-une de ces méthodes (moins d’une dizaine).
La méthode methods
nous donne la liste des méthodes d’un objet, la méthode respond_to?
, quant à elle, nous permet de savoir si un objet a une méthode. Elle prend en paramètre un symbole (ou une chaîne de caractère), celui correspondant à la méthode dont on veut tester l’existence, et renvoie un booléen, true
si la méthode existe, false
sinon. Par exemple, notre objet possède une méthode respond_to
.
print 'Je possède une méthode respond_to?.' if message.respond_to?(:respond_to?)
On peut alors afficher le contenu du message s’il existe et un autre message sinon.
if message.respond_to?(:content)
print message.content
else
print 'Ce message n’a pas de contenu. On devrait peut-être le supprimer ?'
end
Ces deux méthodes nous permettent de faire ce qu’on appelle de l’introspection, c’est-à-dire qu’elles nous permettent d’examiner les objets manipulés. En fait, l’objet s’examine lui-même, d’où le nom d’introspection.
Une autre méthode qui est à connaître est la méthode inspect
. Cette méthode renvoie une chaîne de caractères et ressemble beaucoup à to_s
. En fait, quand irb
affiche le contenu d’un objet ou d’une opération, c’est la méthode inspect
qui est appelé. Nous avons également la méthode p
qui est le pendant de puts
, mais avec la méthode inspect
.
puts message
# => Message de Karnaj à destination de Zeste de Savoir. Contenu secret.
p message
# => <Object:un_nombre>
On voit bien qu’on n’obtient pas le même résultat, et c’est normal, nous n’avons pas redéfini inspect
, mais seulement to_s
.
Mais à quoi sert inspect
?
C’est vrai que la question se pose, d’autant plus que to_s
et inspect
donnent souvent le même résultat. En gros, to_s
est là pour de l’affichage là où inspect
est majoritairement utilisé pour du débogage.
Comparaison
Ici, nous allons faire un retour sur la comparaison d’objets. En effet, une question que nous pouvons nous poser et à laquelle il est important de répondre est comment comparer deux objets. Deux nombres sont égaux s’ils sont les mêmes, deux chaînes de caractères sont égales si elles ont les mêmes caractères, deux tableaux sont égaux s’ils ont les mêmes éléments, mais à quel moment deux objets que l’on vient de créer sont égaux ?
a = Object.new
b = Object.new
a == b # => false
Ici, on obtient false
. Pourtant ces deux objets sont exactement la même chose. On pourrait penser que c’est parce que ce sont deux objets distincts. Cette réflexion n’est pas bête, et c’est en effet à cause de ça. Mais dans ce cas, le résultat obtenu avec les chaînes posent problème.
a = 'abc'
b = 'abc'
a == b # => true
a.object_id == b.object_i # false
Ici, a
et b
ne font pas référence au même objet puisqu’ils n’ont pas le même identifiant.
En fait, pour vérifier que deux objets sont égaux nous utilisons juste… une méthode. Écrire a == b
est strictement équivalent à écrire a.== b
. Et donc, puisque c’est une méthode, on peut redéfinir ==
. Par exemple, on peut redéfinir la méthode d’un message.
def message.==(other)
message.content == other.content
end
La méthode ==
prend en paramètre un autre message et retourne true
si les contenus des deux messages sont égaux.
En fait, nous disposons également d’autres manières de tester si deux objets sont égaux.
Égalité d’identité
Pour commencer, on a l’égalité d’identité, associé à la méthode equal?
. Elle permet de tester si deux objets ont la même identité.
a = 2
b = 2
a.equal?(b) # => true
a.object_id == b.object_id # => true
a = [1, 2]
b = [1, 2]
a.equal?(b) # => false
a.object_id == b.object_id # => false
Égalité sricte
L’égalité stricte, associée à la méthode eql?
, permet de savoir si deux objets sont strictement les mêmes. Par exemple, 1 == 1.0
vaut true
mais 1.eql?(1.0)
vaut false
, car les deux objets ne valent pas strictement la même chose.
Cette méthode est rarement utilisée, parce que généralement ==
nous suffit. En pratique, cette comparaison stricte est rarement utile.
Égalité du case
L’égalité du case
associée à la méthode ===
est présente quand on utilise case
. En effet, (1..10) === 7
renvoie true
. On peut alors écrire ces deux codes équivalents.
case hour
when 0..6
print 'Nuit.'
when 7..12
print 'Matin'
when 12..18
print 'Après-midi.'
when 18..24
print 'Soir.'
else
print 'Euh, chez nous les journées ont 24 heures'
end
if (0..6) === hour
print 'Nuit.'
elsif (6..12) === hour
print 'Matin'
elsif (12..18) === hour
print 'Après-midi.'
elsif (18..24) === hour
print 'Soir.'
else
print 'Euh, chez nous les journées ont 24 heures'
end
Le symbole ===
n’est quasiment jamais utilisé et son utilisation est à éviter. Conformément à son nom, il ne sera utilisé que quand on utilisera des case
et son utilisation sera donc implicite (nous ne l’écrirons pas).
Redéfinir toutes ces égalités ?
En fait, tout ce que nous venons de dire là est relatif à l’objet considéré puisque nous pouvons redéfinir ces méthodes comme nous l’avons fait plus haut avec le ==
de notre message. Ainsi, nous pouvons donner à ces égalités la signification que l’on veut. Néanmoins, nous resterons raisonnables et respecterons ces quelques règles.
- Généralement, on ne redéfinit pas
equal?
. - On peut redéfinir
===
conformément à l’utilisation qu’on fait de l’objet dans nos conditions et donc aveccase
.
On rajoute à ces règles celles que l’on a vu plus haut, ce qui nous donne ces règles d’utilisation.
- On n’utilise pas
===
explicitement. - On utilise rarement
equal?
. - On privilégie
==
àeql?
à moins que l’on ait vraiment besoin deeql?
.
En résumé, sauf cas particulier, on ne s’occupe que de ==
.
Envoi de message
En Ruby, plutôt que de parler d’appel de méthode comme dans beaucoup d’autres langages, on préfère parfois parler d’envoi de message. Le point est alors l’opérateur d’envoi de message. Quand on écrit message.auteur
, on envoie alors un message à l’objet message
pour lui demander son auteur. Le message, à droite de l’opérateur d’envoi de message, est envoyé à l’objet, situé à sa gauche.
En fait, ce nom se justifie encore plus lorsqu’on utilise les méthodes public_send
, __send__
ou send
. Tout comme respond_to?
, ces fonctions prennent en paramètre un symbole ou une chaîne de caractère, et demandent à l’objet d’appeler la méthode qui y est associé.
message.public_send(:author)
Cette ligne se comprend aisément comme « envoyer à message
le message author
».
Il y a quelques différences entre les trois méthodes vues. Si nous devons les utiliser, nous préférerons l’utilisation de public_send
(pour des raisons que nous verrons plus tard). Dans les cas où, elle ne peut pas être utilisée (là encore nous verrons ces cas plus tard), nous préférerons __send__
pour deux raisons.
- Certains objets redéfinissent
send
(par exemple, on pourrait redéfinir la méthodesend
de notremessage
pour qu’elle envoie le contenu du message à son destinataire. - Certains objets n’implémentent pas la méthode
send
.
On peut alors écrire ce genre de code grâce à ces méthodes (on demande à l’utilisateur un nom de méthode et on envoie ce message à l’objet).
print('Quelle information à propos du message voulez-vous ? ')
request = gets.chomp.to_i
print message.public_send(request) if message.respond_to?(request)
Tout ceci est par exemple utilisé pour faire de la métaprogrammation (en gros, écrire du code qui écrit lui-même du code).
Nous avons dit plus haut qu’en Ruby, tout était objet, et que les méthodes servaient à manipuler les méthodes. Qu’en est-il de puts
? N’est-ce pas une vraie méthode ?
En fait, lorsque nous écrivons puts
, Ruby comprend $stdout.puts
. $stdout
est une variable globale représentant la sortie standard (standard output). En écrivant puts
, on demande donc un affichage sur la sortie standard (par défaut la console). Notons également l’existence de l’entrée standard $stdin
(standard input) qui nous permet d’écrire $stdin.gets
.
Il existe également des constantes, STDOUT
et STDIN
, qui représentent la sortie et l’entrée standard. La différence avec $stdin
et $stdout
est que même si on modifie ces deux derniers, on pourra toujours afficher notre message sur la console avec STDOUT.puts
. Pour le moment, cela ne nous est guère utile, mais nous apprendrons plus tard à rediriger $stdout
et $stdin
(par exemple vers des fichiers).
Exercices
Pas de vrai exercices pour ce chapitre, pour nous exercer, nous allons juste créer des objets et les méthodes pour communiquer avec eux. Le plus simple est de rester dans IRB et de jouer avec quelques objets.
Dans ce chapitre très long et très général, nous avons appris beaucoup (vraiment beaucoup) de choses. Tout n’est pas à retenir par cœur, mais il ne faut pas perdre ces informations de vue, elles pourront nous être utiles par la suite.
- En Ruby, tout est objet et donc il n’y a que des méthodes. Même
puts
etgets
sont des méthodes (l’objet auquel elles s’appliquent est juste implicite). - On peut créer un objet avec
Object.new
. On peut ensuite lui rajouter des méthodes à la volée (ce qu’on peut aussi faire avec la plupart des objets existants). - On peut inspecter un objet (connaître ses méthodes, savoir si une méthode existe). Ces opérations relèvent de ce que l’on appelle l’introspection.