J'ai un module appelé Notifier
.
module Notifier def self.prepended(host_class) define_method(:take) do |thing, block| r = super(thing) block.call r end end class Player prepend Notifier attr_reader :inventory def take(thing) # ... end end #> @player.take @apple, -> { puts "Taking apple" } #Taking apple #=> #<Inventory:0x00007fe35f608a98...
Il expose une méthode de classe emit_after
. Je l'utilise comme ceci:
class Player prepend Notifier attr_reader :inventory emit_after :take def take(thing) # ... end end
L'intention est qu'en appelant emit_after: take
, le module remplace #take
avec sa propre méthode.
Mais la méthode de l'instance n'est pas remplacée.
Je peux cependant la remplacer explicitement sans utiliser ClassMethods
module Notifier def self.prepended(host_class) host_class.extend(ClassMethods) end module ClassMethods def emit_after(*methods) methods.each do |method| define_method(method) do |thing, block| r = super(thing) block.call r end end end end end
Je sais que ClassMethods # emit_after
est appelé donc je suppose que la méthode est en cours de définition, mais elle n'est jamais appelée.
Je veux créer les méthodes de manière dynamique. Comment puis-je m'assurer que la méthode generate remplace ma méthode d'instance?
3 Réponses :
Ajouter à la classe actuellement ouverte:
module Notifier def self.prepended(host_class) host_class.extend(ClassMethods) end module ClassMethods def emit_after(*methods) # âââââââ HERE prepend(Module.new do methods.each do |method| define_method(method) do |thing, block = nil| super(thing).tap { block.() if block } end end end) end end end class Player prepend Notifier attr_reader :inventory emit_after :take def take(thing) puts "foo" end end Player.new.take :foo, -> { puts "Taking apple" } #â foo # Taking apple
Il peut être trompeur que Notifier
soit toujours ajouté au début. Vous pouvez également utiliser le plus courant include Notifier
(avec self.included
). Il peut également être judicieux d'avoir un seul module anonyme pour remplacer les méthodes au lieu de créer un nouveau module pour chaque appel emit_after
.
@Stefan accumuler des méthodes pour les regrouper dans le module unique nécessiterait TracePoint # new (: end)
et compliquera donc trop la réponse. J'ai essayé d'appliquer le moins de modifications possible au code d'origine.
Il vous suffit de vous souvenir du module anonyme que vous ajoutez et d'envoyer define_method
à ce module.
@Stefan Oh, l'ajout de méthodes au module déjà préfixé a également un effet sur la cible préfixée; merci, je ne savais pas.
Oui, si vous définissez les méthodes directement sur le module ajouté. En revanche, inclure d'autres modules dans le module ajouté ne fonctionnerait pas, c'est-à-dire que vous pouvez ajouter des méthodes, mais vous ne pouvez pas modifier la chaîne d'ancêtres par la suite.
@Stefan "vous ne pouvez pas modifier la chaîne d'ancêtres par la suite" - que je savais et c'est exactement pourquoi j'ai supposé à tort que cela ne fonctionnait pas non plus avec des définitions de méthode explicites.
Qu'en est-il de cette solution:
module Notifier def self.[](*methods) Module.new do methods.each do |method| define_method(method) do |thing, &block| super(thing) block.call if block end end end end end class Player prepend Notifier[:take] def take(thing) puts "I'm explicitly defined" end end Player.new.take(:foo) { puts "I'm magically prepended" } # => I'm explicitly defined # => I'm magically prepended
C'est assez similaire à la solution d'Aleksei Matiushkin, mais la chaîne des ancêtres est un peu plus propre (pas de notificateur "inutile")
C'est comme ça que je ferais ça dans la vraie vie, oui.
"pas de" notificateur "inutile" - peut-être que préfixer Notifier [: take]
est un peu trompeur à cet égard. Il semble que vous ajoutiez Notifier
, alors que vous ajoutez en fait le module anonyme renvoyé par []
. Votre Notifier
n'est en fait qu'un conteneur pour la méthode []
, rien de plus, rien de moins. Il n'apparaît pas dans la chaîne des ancêtres de Player
.
@Stefan en effet. Je devrais arrêter d'essayer de donner des réponses sur Ruby, j'ai l'impression d'oublier les bases du langage :)
@Stefan "peut-être que le préfixe Notifier [: take] est un peu trompeur à cet égard". Est ce que c'est vraiment? Imo, cette astuce peut être trompeuse uniquement lorsque vous la voyez pour la première fois, mais en général, elle est plus simple et prévisible (en termes de chaîne d'ancêtres résultante) que de jouer avec des crochets.
"Imo" ← lol, j'ai lu "lmao" :-) Avec trompeur je veux juste dire qu'il semble que vous soyez en préfixe (une sorte de) Notifier
, mais à la place, un module anonyme indépendant est ajouté au début. Je m'attendrais à ce que prefend Notifier [: take]
aboutisse à Player.ancestors
contenant (une sorte de) Notifier
, c'est tout.
La solution de
@Konstantin Strukov
est bonne mais peut-être un peu déroutante. Donc, je suggère une autre solution, qui ressemble plus à l'original.
Votre premier objectif est d'ajouter une méthode de classe ( emit_after
) à votre classe. Pour ce faire, vous devez utiliser la méthode extend
sans aucun hook tel que self.prepended ()
, self.included ()
ou self .extended ()
.
prefend
, ainsi que include
, sont utilisés pour ajouter ou remplacer des méthodes d'instance . Mais c'est votre deuxième objectif et cela se produit lorsque vous appelez emit_after
. Vous ne devez donc pas utiliser prepend
ou include
lors de l'extension de votre classe.
module Notifier def emit_after(*methods) prepend(Module.new do methods.each do |method| define_method(method) do |thing, &block| super(thing) block.call if block end end end) end end class Player extend Notifier emit_after :take def take(thing) puts thing end end Player.new.take("foo") { puts "bar" } # foo # bar # => nil
Il est maintenant évident que vous appelez étendre Notifier
afin d'ajouter la méthode de classe emit_after
et toute la magie est cachée dans la méthode.
Je viens d'essayer ceci dans Pry mais il ne semble pas que la méthode soit remplacée.
Ah oui, je vois que vous avez utilisé &
avant l'argument block afin que vous n'ayez pas à utiliser shabby lambda.
Merci beaucoup ! C'est exactement ce que je cherchais! Je n'étais pas confiant à cause des commentaires mais cela fonctionne vraiment! (dans Rails 4 au moins)