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.

1
  • I did a major revision, once I figured out some of the fine points about includeing modules. Commented May 21, 2015 at 5:32

3 Answers 3

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 
Sign up to request clarification or add additional context in comments.

5 Comments

I get this error when I try it that way: "bind argument must be an instance of Plugin3 (TypeError)".
I just have copy-pasted this to a file, ran it, and it works (ruby 2.2.0)
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 I'm still stuck on 1.9.3 but will experiment with current version later tonight, I hope. Thanks.
@ShallmentMo Just found this excellent discussion, which notes the change in behavior starting in ruby 2.0: stackoverflow.com/a/4471202/55857 (see "Rebind module method" section).
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.)

Comments

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 

2 Comments

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.
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!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.