Les objets

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 avec case.

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 de eql?.

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.

  1. Certains objets redéfinissent send (par exemple, on pourrait redéfinir la méthode send de notre message pour qu’elle envoie le contenu du message à son destinataire.
  2. 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 et gets 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.