Ruby 2.6:在添加模块时,如何动态覆盖实例方法?

时间:2019-03-19 11:30:11

标签: 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

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

class Player
  prepend Notifier
  attr_reader :inventory

  emit_after :take

  def take(thing)
    # ...
  end
end

目的是通过调用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...

我知道ClassMethods#emit_after被调用,所以我假设该方法已定义,但从未被调用。

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

3 个答案:

答案 0 :(得分:2)

该解决方案如何?

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

这与Aleksei Matiushkin的解决方案非常相似,但是祖先的链条比较干净(那里没有“无用的”通知程序)

答案 1 :(得分:1)

进入当前打开的课程:

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

答案 2 :(得分:1)

@Konstantin Strukov的解决方案不错,但可能有些混乱。因此,我建议另一种解决方案,该解决方案更像原始解决方案。

您的首要目标是向您的班级添加 class方法emit_after)。为此,您应该使用extend方法,不要使用任何钩子,例如self.prepended()self.included()self.extended()

prependinclude用于添加或覆盖实例方法。但这是您的第二个目标,当您致电emit_after时就会发生。因此,在扩展类时,不应使用prependinclude

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

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