On entend souvent dire que la métaprogrammation est quelque chose que seuls les ninjas de Ruby utilisent, et que ce n’est tout simplement pas pour le commun des mortels. Mais la vérité est que la métaprogrammation n’est pas quelque chose d’effrayant du tout. Cet article de blog servira à remettre en question ce type de pensée et à rapprocher la métaprogrammation du développeur Ruby moyen afin qu’il puisse également en tirer les bénéfices.
Il convient de noter que la métaprogrammation pourrait signifier beaucoup de choses et qu’elle peut souvent être très mal utilisée et aller à l’extrême quand il s’agit d’utilisation, donc je vais essayer de jeter quelques exemples du monde réel que tout le monde pourrait utiliser dans la programmation quotidienne.
Métaprogrammation
La métaprogrammation est une technique par laquelle vous pouvez écrire du code qui écrit du code par lui-même dynamiquement au moment de l’exécution. Cela signifie que vous pouvez définir des méthodes et des classes pendant l’exécution. C’est fou, non ? En un mot, en utilisant la métaprogrammation, vous pouvez rouvrir et modifier des classes, attraper des méthodes qui n’existent pas et les créer à la volée, créer du code qui est DRY en évitant les répétitions, et plus encore.
Les bases
Avant de plonger dans la métaprogrammation sérieuse, nous devons explorer les bases. Et la meilleure façon de le faire, c’est par l’exemple. Commençons par un et comprenons la métaprogrammation Ruby étape par étape. Vous pouvez probablement deviner ce que fait ce code :
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Nous avons défini une classe avec deux méthodes. La première méthode de cette classe est une méthode de classe et la seconde est une méthode d’instance. Ce sont des choses basiques en Ruby, mais il se passe beaucoup plus de choses derrière ce code que nous devons comprendre avant de continuer. Il convient de souligner que la classe Developer
elle-même est en fait un objet. En Ruby, tout est un objet, y compris les classes. Puisque Developer
est une instance, elle est une instance de la classe Class
. Here is how the Ruby object model looks like:
p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject
One important thing to understand here is the meaning of self
. The frontend
method is a regular method that is available on instances of class Developer
, but why is backend
method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self
for any given line. self
is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self
refers to the class itself which is an instance of class Class
.
class Developer p self end# Developer
Inside instance methods, self
refers to an instance of the class.
class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>
Dans les méthodes de classe, self
fait référence à la classe elle-même d’une certaine manière (qui sera abordée plus en détail plus loin dans cet article) :
class Developer def self.backend self endendp Developer.backend# Developer
C’est bien, mais qu’est-ce qu’une méthode de classe après tout ? Avant de répondre à cette question, nous devons mentionner l’existence de quelque chose appelé métaclasse, également connu sous le nom de classe singleton et de classe propre. La méthode de classe frontend
que nous avons définie précédemment n’est rien d’autre qu’une méthode d’instance définie dans la métaclasse pour l’objet Developer
! Une métaclasse est essentiellement une classe que Ruby crée et insère dans la hiérarchie d’héritage pour contenir les méthodes de la classe, n’interférant ainsi pas avec les instances qui sont créées à partir de la classe.
Métaclasses
Chaque objet en Ruby a sa propre métaclasse. Elle est en quelque sorte invisible pour un développeur, mais elle est là et vous pouvez l’utiliser très facilement. Puisque notre classe Developer
est essentiellement un objet, elle possède sa propre métaclasse. A titre d’exemple, créons un objet d’une classe String
et manipulons sa métaclasse:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
Ce que nous avons fait ici, c’est que nous avons ajouté une méthode singleton something
à un objet. La différence entre les méthodes de classe et les méthodes singleton est que les méthodes de classe sont disponibles pour toutes les instances d’un objet de classe alors que les méthodes singleton ne sont disponibles que pour cette seule instance. Les méthodes de classe sont largement utilisées alors que les méthodes de singleton ne le sont pas tellement, mais les deux types de méthodes sont ajoutés à une métaclasse de cet objet.
L’exemple précédent pourrait être réécrit comme ceci:
example = "I'm a string object"class << example def something self.upcase endend
La syntaxe est différente mais elle fait effectivement la même chose. Revenons maintenant à l’exemple précédent où nous avons créé la classe Developer
et explorons quelques autres syntaxes pour définir une méthode de classe:
class Developer def self.backend "I am backend developer" endend
C’est une définition de base que presque tout le monde utilise.
def Developer.backend "I am backend developer"end
This is the same thing, we are defining the backend
class method for Developer
. We didn’t use self
but defining a method like this effectively makes it a class method.
class Developer class << self def backend "I am backend developer" end endend
Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String
object. You may notice that we used self
here which refers to a Developer
object itself. First we opened Developer
class, making self equal to the Developer
class. Next, we do class << self
, making self equal to Developer
‘s metaclass. Then we define a method backend
on Developer
‘s metaclass.
class << Developer def backend "I am backend developer" endend
By defining a block like this, we are setting self
to Developer
‘s metaclass for the duration of the block. As a result, the backend
method is added to Developer
‘s metaclass, rather than the class itself.
Let’s see how this metaclass behaves in the inheritance tree:
As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:
class Object def metaclass_example class << self self end endend
If we define an instance method in Object
class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self
referring to the Object
object inside it. Nous pouvons ensuite utiliser la syntaxe class << self
pour changer le self actuel afin qu’il pointe vers la métaclasse de l’objet actuel. Puisque l’objet actuel est Object
classe elle-même, ce serait la métaclasse de l’instance. La méthode retourne self
qui est à ce stade une métaclasse elle-même. Ainsi, en appelant cette méthode d’instance sur n’importe quel objet, nous pouvons obtenir une métaclasse de cet objet. Définissons à nouveau notre classe Developer
et commençons à explorer un peu :
Et pour le crescendo, voyons la preuve que frontend
est une méthode d’instance d’une classe et backend
est une méthode d’instance d’une métaclasse :
Bien que, pour obtenir la métaclasse, vous n’ayez pas besoin de rouvrir réellement Object
et d’ajouter ce hack. Vous pouvez utiliser singleton_class
que Ruby fournit. C’est la même chose que metaclass_example
que nous avons ajouté mais avec ce hack vous pouvez réellement voir comment Ruby fonctionne sous le capot :
p developer.class.singleton_class.instance_methods false#
Définir des méthodes à l’aide de « class_eval » et « instance_eval »
Il y a une autre façon de créer une méthode de classe, et c’est en utilisant instance_eval
:
Ce morceau de code que l’interpréteur Ruby évalue dans le contexte d’une instance, qui est dans ce cas un objet Developer
. Et lorsque vous définissez une méthode sur un objet, vous créez soit une méthode de classe, soit une méthode singleton. Dans ce cas, il s’agit d’une méthode de classe – pour être exact, les méthodes de classe sont des méthodes singleton mais des méthodes singleton d’une classe, tandis que les autres sont des méthodes singleton d’un objet.
D’autre part, class_eval
évalue le code dans le contexte d’une classe au lieu d’une instance. Il rouvre pratiquement la classe. Voici comment class_eval
peut être utilisé pour créer une méthode d’instance :
En résumé, lorsque vous appelez la méthode class_eval
, vous changez self
pour faire référence à la classe d’origine et lorsque vous appelez instance_eval
self
change pour faire référence à la métaclasse de la classe d’origine.
Définir les méthodes manquantes à la volée
Une autre pièce du puzzle de la métaprogrammation est method_missing
. Lorsque vous appelez une méthode sur un objet, Ruby va d’abord dans la classe et parcourt ses méthodes d’instance. S’il ne trouve pas la méthode à cet endroit, il poursuit sa recherche en remontant la chaîne des ancêtres. Si Ruby ne trouve toujours pas la méthode, il appelle une autre méthode nommée method_missing
qui est une méthode d’instance de Kernel
dont chaque objet hérite. Puisque nous sommes sûrs que Ruby va éventuellement appeler cette méthode pour les méthodes manquantes, nous pouvons l’utiliser pour mettre en œuvre certaines astuces.
define_method
est une méthode définie dans la classe Module
que vous pouvez utiliser pour créer des méthodes dynamiquement. Pour utiliser define_method
, vous l’appelez avec le nom de la nouvelle méthode et un bloc où les paramètres du bloc deviennent les paramètres de la nouvelle méthode. Quelle est la différence entre utiliser def
pour créer une méthode et define_method
? Il n’y a pas beaucoup de différence, sauf que vous pouvez utiliser define_method
en combinaison avec method_missing
pour écrire du code DRY. Pour être exact, vous pouvez utiliser define_method
au lieu de def
pour manipuler les scopes lors de la définition d’une classe, mais c’est une toute autre histoire. Voyons un exemple simple :
Ceci montre comment define_method
a été utilisé pour créer une méthode d’instance sans utiliser de def
. Cependant, nous pouvons faire bien plus avec eux. Jetons un coup d’œil à cet extrait de code :
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"
Ce code n’est pas DRY, mais en utilisant define_method
nous pouvons le rendre DRY :
C’est beaucoup mieux, mais pas encore parfait. Pourquoi ? Si nous voulons ajouter une nouvelle méthode coding_debug
par exemple, nous devons mettre cette "debug"
dans le tableau. Mais en utilisant method_missing
, nous pouvons résoudre ce problème :
Ce morceau de code est un peu compliqué, alors décomposons-le. L’appel d’une méthode qui n’existe pas déclenchera method_missing
. Ici, nous voulons créer une nouvelle méthode uniquement lorsque le nom de la méthode commence par "coding_"
. Sinon, nous appelons simplement super pour faire le travail de signalement d’une méthode qui est en fait manquante. Et nous utilisons simplement define_method
pour créer cette nouvelle méthode. Voilà, c’est fait ! Avec ce morceau de code, nous pouvons créer littéralement des milliers de nouvelles méthodes en commençant par "coding_"
, et ce fait est ce qui rend notre code DRY. Puisque define_method
se trouve être privée à Module
, nous devons utiliser send
pour l’invoquer.
Wrapping up
Ce n’est que la pointe de l’iceberg. Pour devenir un Jedi Ruby, c’est le point de départ. Après avoir maîtrisé ces blocs de construction de la métaprogrammation et vraiment compris son essence, vous pourrez passer à quelque chose de plus complexe, par exemple créer votre propre langage spécifique au domaine (DSL). Le DSL est un sujet en soi, mais ces concepts de base sont une condition préalable à la compréhension des sujets avancés. Certaines des gemmes les plus utilisées dans Rails ont été construites de cette manière et vous avez probablement utilisé son DSL sans même le savoir, comme RSpec et ActiveRecord.
Espérons que cet article puisse vous rapprocher de la compréhension de la métaprogrammation et peut-être même de la construction de votre propre DSL, que vous pourrez utiliser pour coder plus efficacement.