首页 > 解决方案 > Ruby 2.6:如何在添加模块时动态覆盖实例方法?

问题描述

我有一个名为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被调用了,所以我假设该方法正在被定义,但它永远不会被调用。

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

标签: rubymetaprogramming

解决方案


@Konstantin Strukov的解决方案很好,但可能有点混乱。所以,我建议另一种解决方案,它更像原来的解决方案。

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

prepend, 以及include, 用于添加或覆盖实例方法。但这是你的第二个目标,它发生在你打电话时emit_after。所以你不应该使用prependorinclude扩展你的课程。

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类方法,所有的魔法都隐藏在方法中。


推荐阅读