Ruby 2.6:在添加模块时如何动态重写实例方法?

use*_*603 3 ruby metaprogramming

我有一个名为 的模块Notifier

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
Run Code Online (Sandbox Code Playgroud)

它公开了一个类方法emit_after。我这样使用它:

class Player
  prepend Notifier
  attr_reader :inventory

  emit_after :take

  def take(thing)
    # ...
  end
end
Run Code Online (Sandbox Code Playgroud)

目的是通过调用emit_after :take,模块#take用它自己的方法覆盖。

但实例方法没有被重写。

但是,我可以显式覆盖它而不使用ClassMethods

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...
Run Code Online (Sandbox Code Playgroud)

我知道它ClassMethods#emit_after被调用,所以我假设该方法正在被定义,但它永远不会被调用。

我想动态创建方法。如何确保生成方法覆盖我的实例方法?

chu*_*off 6

@Konstantin Strukov的解决方案很好,但可能有点令人困惑。因此,我建议另一种解决方案,它更像原来的解决方案。

您的第一个目标是向您的类添加一个类方法( emit_after)。为此,您应该使用extend不带任何钩子的方法,例如self.prepended(),self.included()self.extended()

prepend以及include用于添加或重写实例方法。但这是你的第二个目标,当你打电话时它就会发生emit_after。所以你不应该在扩展你的类时使用prependor include

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
Run Code Online (Sandbox Code Playgroud)

现在很明显,您调用是extend Notifier为了添加emit_after类方法,并且所有魔法都隐藏在该方法中。