Démêler les classes, les Instances et les Métaclasses dans Ruby

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.



+