3
votes

Ruby 2.6: Comment puis-je remplacer dynamiquement les méthodes d'instance lors de l'ajout d'un module?

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?


0 commentaires

3 Réponses :


1
votes

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


6 commentaires

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.



3
votes

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")


5 commentaires

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.



4
votes

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.


3 commentaires

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)