Métaprogrammation et réflectivité Ruby
ruby est particulièrement appréciable pour sa dynamicité C'est ce que nous allons voir dans cet article avec une présentation de la métaprogrammation et de la réflexivité dans ce langage.
Qu'est-ce que la métaprogrammation et la réflexivité ?
Le livre The Ruby Programming Language chez O'Reilly dit :
Loosely defined, metaprogramming is writing programs (or frameworks) that help you write programs.
To put it another way, metaprogramming is a set of ways for extending ruby's syntax in ways that make programming easier.
Et en Français :
En définissant rapidement, la métaprogrammation consiste à créer des programmes (ou des frameworks) qui aident à écrire des programmes. Pour dire autrement, la métaprogrammation est une liste de méthodes permettant d'étendre la syntaxe de ruby afin de faciliter le développement.
La réflexivité consiste examiner l'état et la structure d'un objet. Ainsi, un programme pourra obtenir la liste de toutes les méthodes définies dans un objet
En gros : nous allons voir, dans cet article, comment un objet peut analyser son état ... Et altérer celui-ci en créant ou remplaçant des méthodes.
Réflectivité
Supposons la classe suivante :
class Testing < Hash
def basic_method
"Hello World"
end
end
Plusieurs méthodes de réflexivité nous sont alors disponibles pour celle-ci.
t = Testing.new
t.class #=> Testing
t.superclass #=> Hash
t.instance_of? Testing #=> true
t.is_a? Hash #=> true
t.respond_to? :basic_method #=> true
Quelles sont ces diverses méthodes que nous exécutons ici ?
- class - Retourne la classe de l'objet. Ici, Testing.
- superclass - Retourne la classe parente de l'objet. Ici, Hash.
- instance_of? - Détermine si l'objet est de la classe passée en paramètre.
- is_a?/kind_of? - Détermine si l'objet est de la classe passée en paramètre ou d'une classe qui en hérite. Si il s'agit d'un module, elle indique si la classe inclus celui-ci.
- respond_to? - Indique si l'objet définit la méthode (privée ou protégée) dont le nom est passé en paramètre. Il est possible de passer un second paramètre true si vous souhaitez également vérifier les méthodes privées.
Ajouté à celles-ci sont disponibles divers raccourcis permettant de tester de manière similaire divers objets.
Supposons les modules et la classe suivante :
module A
end
module B
include A
end
class C
include B
end
Nous avons alors accès aux méthodes suivantes :
C < B #=> true
B < A #=> true
C < A #=> true
Avec l'opérateur <, nous vérifions si une classe hérite d'une autre. Ou bien qu'un module est inclus.
Le test est bien évidemment fait pour toutes les classes parentes.
Bien évidemment, nous pouvons également récupérer une liste de toutes les classes parentes à la notre.
Si nous reprenons notre classe Testing plus haut, nous aurons donc :
Testing.ancestors #=> [Hash]
Qui nous retourne un tableau de toutes les classes dont dépends la classe courante.
L'équivalent pour les modules existe également, nous premettant d'obtenir la liste de tous les modules inclus dans la classe ou le module fourni.
A.included_modules #=> []
B.includes_modules #=> [A]
C.includes_modules #=> [B, A, Kernel]
Enfin une fonction booléen existe également pour savoir si la classe ou le module inclus la classe ou le module fournis.
C.includes?(A) #=> true
Avec ces fonctions de réflexivité, vous pouvez ainsi analyser vos classes lors de l'exécution de votre programme.
Ceci est particulièrement pratique lors de l'écriture de tests mais peut également s'avérer très utile lorsque vous créez dynamiquement de nouvelles méthodes.
Evaluation de code
Chaines de caractères et blocs
L'une des fonctionnalités les plus puissantes, mais aussi les plus dangereuses de Ruby est la méthode eval.
Celle-ci vous permet d'évaluer une chaine de caractères comme s'il s'agissant de code ruby.
x = 1
eval "x + 1" #=> 2
Eval est particulièrement puissant. Mais à moins que vous ne cherchiez à écrire un shell ruby (tel que irb), vous ne l'utiliserez jamais.
instance_eval, class_eval et module_eval
Instance_eval et class_eval sont, quant à eux, beaucoup plus utilisés. Dans rails par exemple, supposons que vous ayez, dans vos routes :
resources :users
Cette route de ressources, créera plusieurs méthodes accessibles dans les contrôleurs et dans les vues :
users_path
users_url
user_path(user)
user_url(user)
edit_user_path(user)
edit_user_url(user)
new_user_path
new_user_url
Quand les quatre premières sont gerées différemment, les quatre dernières sont générées dynamiquement. Vous pouvez voir ceci à la ligne 145 de polymorphic_routes.rb
instance_eval, class_eval et module_eval permettent de modifier dynamiquement le contenu d'un module, d'une classe ... Ou d'une instance de classe.
Ainsi, nous pouvons ajouter une nouvelle méthode dans une chaine de caractères, nous permettant ainsi de la nommer de la manière que nous désirons.
class_eval et module_eval
nous avons module_eval utilisé dans rails plus haut. Class_eval fonctionne de la même manière pour un objet.
Lorsqu'il est utilisé, il permet de modifier dynamiquement le contenu d'un module ou d'une classe ... Et de répercuter ces modifications sur tous les modules ou classes qui incluent celui-ci on en héritent.
Voyons par exemple :
class A
end
class B < A
end
La méthode "hello" n'est pas définie dans la classe A. Donc elle ne l'est pas non plus dans la classe B.
B.hello #=> NoMethodError
Nous pouvons avec class_eval définir celle-ci pour A et pour B.
A.class_eval do
def self.hello
"world"
end
end
La méthode hello est maintenant définie et nous pouvons l'utiliser dans A et toutes les classes qui en étendent.
A.hello #=> "world"
B.hello #=> "world"
module_eval fonctionne exactement de la même manière en modifiant toutes les classes qui le module.
instance_eval
Instance_eval est encore plus puissant puisque, comme son nom l'indique, il permet de modifier une seule instance d'une classe ...
Reprenons nos classes A et B de tout à l'heure.
class A
end
class B < A
end
Et supposons le code suivant :
b = B.new
c = B.new
Maintenant, nous utilisons instance_eval afin de définir une méthode hello sur l'instance "b" de la classe B.
b.instance_eval do
def hello
"world"
end
end
Et maintenant si nous faisions appel à cette methode :
b.hello #=> "world"
c.hello #=> NoMethodError
Les deux instances "b" et "c" sont tous des instances de B.
Mais maintenant que nous avons utilisé instance_eval, elles n'ont plus les même méthodes.
Supprimer des méthodes
Nous avons vu comment définir de nouvelles méthodes.
La logique veut que, si nous pouvons ajouter de nouvelles méthodes à un objet, nous puissions également en supprimer.
C'est ce que permettant remove_method et undef_method.
Remove_method supprime la méthode spécifiée de la classe. Mais si la même méthode est définie par une classe parente alors, c'est celle-ci qui sera maintenant appelée.
Under_method est plus stricte. Il supprime la méthode dans la classe fournie mais également dans toutes les classes parentes.
Aliaser des méthodes
Lorsque vous surchargez une méthode, c'est probablement dans le but de modifier son fonctionnement originel ou d'y ajouter une fonctionnalité.
Mais dans ce cas, vous perdez la fonctionnalité originelle et êtes obligé de la réimplémenter.
Afin d'éviter cela, il est possible d'aliaser des méthodes.
Supposons la classe suivante :
class B
def hello
"world"
end
end
Supposons maintenant que vous souhaitiez que cette méthode hello ne retourne plus "world" mais "hello world".
Nous allons évidemment utiliser class_eval afin de surcharger la classe. Cependant nous souhaitons utiliser le code déjà implémenté dans la classe originelle.
B.class_eval do
alias :old_hello :hello
end
Ce code va dynamiquement faire une copie de la méthode hello dans une nouvelle méthode old_hello. Nous pouvons donc maintenant surcharger notre classe sereinement, nous avons toujours accès à l'ancienne méthode.
B.class_eval do
alias :old_hello :hello
def hello
"hello #{old_hello}"
end
end
Si maintenant, nous faisons appel à B, nous aurons :
B.new.hello #=> "hello world"
Freeze
Vous avez vu que nous pouvons modifier autant que nous le désirons toute classe.
Cependant vous pouvez désirer, pour une raison ou une autre, que l'une de vos classes ne doit surtout pas être modifiée.
class B; end
B.freeze
B.class_eval do
def hello
"world"
end
end #=> TypeError: can't modify frozen class
Vous pouvez ainsi empêcher volontairement toute modification dynamique de l'une de vos classes.
Conclusion
Comme vous pouvez le constater, les fonctionnalités dynamiques de ruby sont particulièrement puissantes.
Attention cependant. Avec de grands pouvoirs viennent de grandes responsabilités.
Définir dynamiquement des méthodes à mauvais escient peut fortement nuire à la vitesse de votre application, voir à sa sécurité.
Réflechissez-y à deux fois avant de modifier dynamiquement l'une de vos classes.
L'image ci-contre est l'interface de recherche de l'application sur laquelle je travaille.
