如何像调用实例方法一样动态调用模块方法?

How do you dynamically invoke a module method as if it were an instance method?

我正在开发一种工具,它在 class(称之为 Runner)中提供通用功能,并且可以使用一种插件系统调用用户定义的代码。对于该工具的任何执行,我需要动态执行由一个或多个插件定义的各种方法。因为 Runner class 定义了插件中需要的许多实例级属性,所以我想 像实例方法一样执行插件方法 Runner.

这是一个简化的例子:

module Plugin1
  def do_work
    p ['Plugin1', data]
  end
end

module Plugin2
  def do_work
    p ['Plugin2', data]
  end
end

module Plugin3
  def do_work
    p ['Plugin3', data]
  end
end

class Runner
  attr_accessor :data  # Plugins need access to these.

  def initialize(data, *plugins)
    @data = data
    @plugin_names = plugins.map { |p| "Plugin#{p}" }
  end

  def run
    @plugin_names.each { |name|
      mod = Kernel.const_get(name)
      plugin_method = mod.instance_method(:do_work)
      # How do I call the plugin_method as if it were
      # an instance method of the Runner?
    }
  end
end

# Execute a runner using Plugin3 and Plugin1.
r = Runner.new(987, 3, 1)
r.run

我已经使用 instance_execbindmodule_function 等尝试了各种方法来实现这一目标,但没有任何效果。当然,我对该工具的其他方法持开放态度,但我也很好奇是否可以按照上述方式完成此操作。

这应该有效。它动态包含模块

module Plugin1
  def do_work
    p ['Plugin1', data]
  end
end

module Plugin2
  def do_work
    p ['Plugin2', data]
  end
end

module Plugin3
  def do_work
    p ['Plugin3', data]
  end
end

class Runner
  attr_accessor :data  # Plugins need access to these.

  def initialize(data, *plugins)
    @data = data
    @plugin_names = plugins.map { |p| "Plugin#{p}" }
  end

  def run
    @plugin_names.each { |name|
      mod = Kernel.const_get(name)
      Runner.send(:include, mod)
      do_work
    }
  end
end

# Execute a runner using Plugin3 and Plugin1.
r = Runner.new(987, 3, 1)
r.run

我觉得用bind是对的,不知道你为什么觉得不行

module Plugin1
  def do_work
    p ['Plugin1', data]
  end
end

module Plugin2
  def do_work
    p ['Plugin2', data]
  end
end

module Plugin3
  def do_work
    p ['Plugin3', data]
  end
end

class Runner
  attr_accessor :data  # Plugins need access to these.

  def initialize(data, *plugins)
    @data = data
    @plugin_names = plugins.map { |p| "Plugin#{p}" }
  end

  def run
    @plugin_names.each { |name|
      mod = Kernel.const_get(name)
      plugin_method = mod.instance_method(:do_work).bind(self)
      # How do I call the plugin_method as if it were
      # an instance method of the Runner?
      plugin_method.call
    }
  end
end

# Execute a runner using Plugin3 and Plugin1.
r = Runner.new(987, 3, 1)
r.run

你可以使用 Module#alias_method:

module Other
  def do_work
    puts 'hi'
  end
end

module Plugin1
  def do_work
    p ['Plugin1', data]
  end
end

module Plugin2
  def do_work
    p ['Plugin2', data]
  end
end

module Plugin3
  def do_work
    p ['Plugin3', data]
  end
end

class Runner
  include Other
  attr_accessor :data

  def initialize(data, *plugins)
    @data = data
    @plugin_names = plugins.map { |p| "Plugin#{p}" }
  end

  def self.save_methods(mod, alias_prefix)
    (instance_methods && mod.instance_methods).each { |m|
      alias_method :"#{alias_prefix}#{m.to_s}", m }
  end

  def self.include_module(mod)
    include mod
  end

  def self.remove_methods(mod, alias_prefix)
    ims = instance_methods
    mod.instance_methods.each do |m|
      mod.send(:remove_method, m)
      aka = :"#{alias_prefix}#{m.to_s}"
      remove_method(aka) if ims.include?(aka) 
    end
  end

  def run(meth)
    alias_prefix = '_old_'
    @plugin_names.each do |mod_name|
      print "\ndoit 1: "; send(meth)  # included for illustrative purposes
      mod_object = Kernel.const_get(mod_name)
      self.class.save_methods(mod_object, alias_prefix)
      self.class.include_module(mod_object)
      print "doit 2: "; send(meth)  # included for illustrative purposes
      self.class.remove_methods(mod_object, alias_prefix)
    end
  print "\ndoit 3: "; send(meth)  # included for illustrative purposes
  end
end

试一试:

r = Runner.new(987, 3, 1)
r.run(:do_work)
  #-> doit 1: hi
  #   doit 2: ["Plugin3", 987]

  #   doit 1: hi
  #   doit 2: ["Plugin1", 987]

  #   doit 3: hi

在每个模块 modincluded 并且执行任何感兴趣的计算之后,mod.remove_method m 将应用于 mod 中的每个方法。这实际上 "uncovers" Runner 中的实例方法在 mincluded 时被 m 覆盖。比方说,之前 Other#do_work 是 "overwritten"(不是正确的词,因为该方法仍然存在),在 Runner 中创建了一个别名 _old_do_work。由于在删除 Plugin1#do_word 时会发现 Other#do_work,因此没有必要也不希望有 alias_method :do_word, :_old_do_work。只应删除 alias

(对于 运行 上面的代码,有必要剪切并粘贴我插入的几乎为空的行分隔的三个部分以避免垂直滚动。)