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)