Boire ou coder ... Pourquoi choisir?
Publié le 06 septembre 2010 13:33

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.

Plus que "vous ne l'utiliserez jamais", on devrait même dire "vous ne devriez jamais l'utiliser". C'est la meilleure manière d'obtenir des failles de sécurité gigantesques. Qui plus est, il est toujours possible de faire sans.

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.

Commentaires

Michel Pigassou
Michel Pigassou dit: 08 septembre 2010 07:15 Site web

Très bon article. Toutefois, ne parle-t-on pas plutôt de "réflexion" (à la place de réflexivité) ? :)

Postez un commentaire

Markdown activé