1

I'm working on a tool that provides common functionality in a class (call it Runner) and that can invoke user-defined code using a kind of plugin system. For any execution of the tool, I need to dynamically execute various methods defined by one or more plugins. Because the Runner class defines many instance-level attributes that will be needed in the plugins, I would like to execute the plugin methods as if they were instance methods of Runner.

Here is a simplified example:

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

I have experimented with various ways to pull this off using instance_exec, bind, module_function, and so forth, but haven't gotten anything to work. I'm open to other approaches for the tool, of course, but I'm also curious whether this can be done in the manner described above.

FMc
  • 41,963
  • 13
  • 79
  • 132

3 Answers3

2

I think it's right to use bind, but I don't know why you think it didn't work

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
ShallmentMo
  • 449
  • 1
  • 4
  • 15
  • I get this error when I try it that way: "bind argument must be an instance of Plugin3 (TypeError)". – FMc May 19 '15 at 16:37
  • I just have copy-pasted this to a file, ran it, and it works (ruby 2.2.0) – Eugene Petrov May 20 '15 at 01:43
  • It could be the problem of the version of ruby. I'm not sure, but it work for me. Mine is ruby 2.1.0 – ShallmentMo May 20 '15 at 01:59
  • @ShallmentMo I'm still stuck on 1.9.3 but will experiment with current version later tonight, I hope. Thanks. – FMc May 20 '15 at 02:15
  • @ShallmentMo Just found this excellent discussion, which notes the change in behavior starting in ruby 2.0: http://stackoverflow.com/a/4471202/55857 (see "Rebind module method" section). – FMc May 20 '15 at 02:56
1

This should work. It dynamically includes the module

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
chintanparikh
  • 1,632
  • 6
  • 22
  • 36
  • Thanks. Perhaps my simplified example is too simple. I've been avoiding a straightforward use of `include` due to concerns that user-defined methods might collide with each other. In some sense, `include` brings **too much** into the `Runner`. – FMc May 19 '15 at 16:09
  • Ah okay that makes sense. I'm not sure it's going to be possible without either making Runner a singleton, or creating a class that wraps Runner because `@data` is an instance method, and as far as I know, there's no way to access an instance method from a class definition (you can even define `@data` inside the class, outside methods, and inside a method and they're essentially two different variables because of scope). I could be wrong though, so please let me know if you figure something out! – chintanparikh May 19 '15 at 16:18
1

You could use 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

Try it:

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

After each module mod is included, and any calculations of interest are performed, mod.remove_method m is applied to each method in mod. This in effect "uncovers" the instance method in Runner that was overwritten by m when m was included. Before, say, Other#do_work was "overwritten" (not the right word, as the method is still there), an alias _old_do_work was made in Runner. As Other#do_work is uncovered when Plugin1#do_word is removed, it is neither necessary nor desirable to have alias_method :do_word, :_old_do_work. Only the alias should be removed.

(To run the code above, it is necessary to cut and paste the three sections divided by almost-empty lines that I inserted to avoid vertical scrolling.)

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100