Bienvenue dans un nouvel épisode de Ruby Magic ! L’édition de ce mois-ci est consacrée aux métaclasses, un sujet suscité par une discussion entre deux développeurs (Salut Maud !).
En examinant les métaclasses, nous apprendrons comment fonctionnent les méthodes de classe et d’instance dans Ruby. En cours de route, découvrez la différence entre définir une méthode en passant un « definee » explicite et en utilisant class << self
ou instance_eval
. Allons-y!
Instances de classe et méthodes d’instance
Pour comprendre pourquoi les métaclasses sont utilisées dans Ruby, nous allons commencer par examiner les différences entre les méthodes d’instance et de classe.
Dans Ruby, une classe est un objet qui définit un plan pour créer d’autres objets. Les classes définissent les méthodes disponibles sur n’importe quelle instance de cette classe.
Définir une méthode à l’intérieur d’une classe crée une méthode d’instance sur cette classe. Toute instance future de cette classe aura cette méthode disponible.
1 2 3 4 5 6 7 8 9 10 11 12 |
class User def initialize(name) @name = name end def name @name end end user = User.new('Thijs') user.name # => "Thijs" |
Dans cet exemple, nous créons une classe nommée User
, avec une méthode d’instance nommée #name
qui renvoie le nom de l’utilisateur. En utilisant la classe, nous créons ensuite une instance de classe et la stockons dans une variable nommée user
. Puisque user
est une instance de la classe User
, la méthode #name
est disponible.
Une classe stocke ses méthodes d’instance dans sa table de méthodes. Toute instance de cette classe fait référence à la table de méthodes de sa classe pour accéder à ses méthodes d’instance.
Objets de classe
Une méthode de classe est une méthode qui peut être appelée directement sur la classe sans avoir à créer une instance au préalable. Une méthode de classe est créée en préfixant son nom par self.
lors de sa définition.
Une classe est elle-même un objet. Une constante fait référence à l’objet de classe, de sorte que les méthodes de classe définies sur celui-ci peuvent être appelées de n’importe où dans l’application.
1 2 3 4 5 6 7 8 9 |
class User # ... def self.all end end User.all # => |
Les méthodes définies avec un préfixe self.
ne sont pas ajoutées à la table de méthodes de la classe. Ils sont plutôt ajoutés à la métaclasse de la classe.
Métaclasses
Mis à part une classe, chaque objet de Ruby a une métaclasse cachée. Les métaclasses sont des singletons, ce qui signifie qu’elles appartiennent à un seul objet. Si vous créez plusieurs instances d’une classe, elles partageront la même classe, mais elles auront toutes des métaclasses distinctes.
1 2 3 4 5 6 7 8 9 |
thijs, robert, tom = User.all thijs.class # => User robert.class # => User tom.class # => User thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>> robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>> tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>> |
Dans cet exemple, nous voyons que bien que chacun des objets ait la classe User
, leurs classes singleton ont des ID d’objet différents, ce qui signifie qu’ils sont des objets séparés.
En ayant accès à une métaclasse, Ruby permet d’ajouter des méthodes directement aux objets existants. Cela n’ajoutera pas de nouvelle méthode à la classe de l’objet.
1 2 3 4 5 6 7 8 |
robert = User.new("Robert") def robert.last_name "Beekman" end robert.last_name # => "Beekman" User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>) |
Dans cet exemple, nous ajoutons un #last_name
à l’utilisateur stocké dans la variable robert
. Bien que robert
soit une instance de User
, toutes les instances nouvellement créées de User
n’auront pas accès à la méthode #last_name
, car elle n’existe que sur la métaclasse de robert
.
Qu’Est-Ce Que le soi?
Lors de la définition d’une méthode et du passage d’un récepteur, la nouvelle méthode est ajoutée à la métaclasse du récepteur, au lieu de l’ajouter à la table de méthodes de la classe.
1 2 3 4 5 |
tom = User.new("Tom") def tom.last_name "de Bruijn" end |
Dans l’exemple ci-dessus, nous avons ajouté #last_name
directement sur l’objet tom
, en passant tom
comme récepteur lors de la définition de la méthode.
C’est aussi ainsi que cela fonctionne pour les méthodes de classe.
1 2 3 4 5 6 7 |
class User # ... def self.all end end |
Ici, nous passons explicitement self
en tant que récepteur lors de la création de la méthode .all
. Dans une définition de classe, self
fait référence à la classe (User
dans ce cas), de sorte que la méthode .all
est ajoutée à la métaclasse de User
.
Comme User
est un objet stocké dans une constante, nous accéderons au même objet — et à la même métaclasse — chaque fois que nous le référencerons.
Ouverture de la métaclasse
Nous avons appris que les méthodes de classe sont des méthodes de la métaclasse de l’objet classe. Sachant cela, nous examinerons d’autres techniques de création de méthodes de classe que vous auriez pu voir auparavant.
class < < self
Bien qu’elle soit un peu démodée, certaines bibliothèques utilisent class << self
pour définir des méthodes de classe. Cette astuce de syntaxe ouvre la métaclasse de la classe actuelle et interagit directement avec elle.
1 2 3 4 5 6 7 8 9 10 11 |
class User class << self self # => #<Class:User> def all end end end User.all # => |
Cet exemple crée une méthode de classe nommée User.all
en ajoutant une méthode à la métaclasse de User
. Au lieu de passer explicitement un récepteur pour la méthode comme nous l’avons vu précédemment, nous définissons self
sur la métaclasse de User
au lieu de User
elle-même.
Comme nous l’avons appris précédemment, toute définition de méthode sans récepteur explicite est ajoutée en tant que méthode d’instance de la classe actuelle. À l’intérieur du bloc, la classe actuelle est la métaclasse User
(#<Class:User>
).
instance_eval
Une autre option consiste à utiliser instance_eval
, qui fait la même chose avec une différence majeure. Bien que la métaclasse de la classe reçoive les méthodes définies dans le bloc, self
reste une référence à la classe principale.
1 2 3 4 5 6 7 8 9 10 11 |
class User instance_eval do self # => User def all end end end User.all # => |
Dans cet exemple, nous définissons une méthode d’instance sur la métaclasse de User
comme précédemment, mais self
pointe toujours sur User
. Bien qu’il pointe généralement vers le même objet, le « default definee » et self
peuvent pointer vers différents objets.
Ce que nous avons appris
Nous avons appris que les classes sont les seuls objets pouvant avoir des méthodes, et que les méthodes d’instance sont en fait des méthodes sur la métaclasse d’un objet. Nous savons que class << self
échange simplement self
pour vous permettre de définir des méthodes sur la métaclasse, et nous savons que instance_eval
fait la même chose (mais sans toucher self
).
Bien que vous ne travailliez pas explicitement avec des métaclasses, Ruby les utilise largement sous le capot. Savoir ce qui se passe lorsque vous définissez une méthode peut vous aider à comprendre pourquoi Ruby se comporte comme elle le fait (et pourquoi vous devez préfixer les méthodes de classe avec self.
).
Merci d’avoir lu. Si vous avez aimé ce que vous avez lu, vous pouvez vous abonner à Ruby Magic pour recevoir un e-mail lorsque nous publions un nouvel article environ une fois par mois.