如何动态调用模块方法,就像它是一个实例方法一样?

时间:2015-05-19 14:59:54

标签: ruby metaprogramming

我正在开发一种工具,它在类中提供通用功能(称之为Runner),并且可以使用一种插件系统调用用户定义的代码。对于该工具的任何执行,我需要动态执行由一个或多个插件定义的各种方法。因为Runner类定义了插件中需要的许多实例级属性,所以我想执行插件方法,就好像它们是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等来解决这个问题,但是还没有任何工作要做。当然,我对这个工具的其他方法持开放态度,但我也很好奇这是否可以按照上述方式完成。

3 个答案:

答案 0 :(得分:2)

我认为使用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

答案 1 :(得分:1)

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

B.prototype = Object.create(A.prototype);

答案 2 :(得分:1)

您可以使用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

在每个模块modinclude d后,执行任何感兴趣的计算后,mod.remove_method m将应用于mod中的每个方法。当Runnerm时,这实际上“揭露”m中被include覆盖的实例方法。之前,比如Other#do_work被“覆盖”(不是正确的词,因为该方法仍然存在),在_old_do_work中创建了别名Runner。当Other#do_work被移除时,Plugin1#do_word被发现,alias_method :do_word, :_old_do_work既不必要也不可取。{1}}。只应移除alias

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