{"id":1186,"date":"2009-12-01T10:28:15","date_gmt":"2009-12-01T09:28:15","guid":{"rendered":"http:\/\/www.devco.net\/?p=1186"},"modified":"2010-08-17T12:28:17","modified_gmt":"2010-08-17T11:28:17","slug":"ruby_plugin_architectures","status":"publish","type":"post","link":"https:\/\/www.devco.net\/archives\/2009\/12\/01\/ruby_plugin_architectures.php","title":{"rendered":"Ruby Plugin Architectures"},"content":{"rendered":"

Most of the applications I write in Ruby are some kind of Framework, ruby-pdns<\/a> takes plugins, mcollective<\/a> takes plugins, my nagios notification bot takes plugins etc, yet I have not yet figured out a decent approach to handling plugins.<\/p>\n

Google suggests many options, the most suggested one is something along these lines.<\/p>\n

<\/p>\n

\r\nclass Plugin\r\n    def self.inherited(klass)\r\n        PluginManager << klass.new\r\n    end\r\nend\r\n\r\nclass FooPlugin\n

<\/code><\/p>\n

Where PluginManager is some class or module that stores and later allows retrieval, when the FooPlugin class gets created it will trigger the hook in the base class.<\/p>\n

This works ok, almost perfectly, except that at the time of the trigger the FooPlugin class is not 100% complete and your constructor will not be called, quite a pain. From what I can tell it calls the constructor on either Class or Object.<\/p>\n

I ended up tweaking the pattern a bit and now have something that works well, essentially if you pass a String to the PluginManager it will just store that as a class name and later create you an instance of that class, else if it's not a string it will save it as a fully realized class assuming that you know what you did.<\/p>\n

The full class is part of mcollective and you can see the source here<\/a> but below the short version:<\/p>\n

I am quite annoyed that including a module does not also include static methods in Ruby, its quite a huge miss feature in my view and there are discussions about changing that behavior. I had hopes of writing something simple that I can just do include Pluggable<\/em> and this would set up all the various bits, create the inherited hook etc, but it's proven to be a pain and would be littered with nasty evals etc.<\/p>\n

<\/p>\n

\r\nmodule PluginManager\r\n   @plugins = {}\r\n\r\n   def self.<<(plugin)\r\n         type = plugin[:type]\r\n         klass = plugin[:class]\r\n    \r\n         raise(\"Plugin #{type} already loaded\") if @plugins.include?(type)\r\n    \r\n         if klass.is_a?(String)\r\n             @plugins[type] = {:loadtime => Time.now, :class => klass, :instance => nil}\r\n         else\r\n             @plugins[type] = {:loadtime => Time.now, :class => klass.class, :instance => klass}\r\n         end\r\n   end\r\n\r\n   def self.[](plugin)\r\n       raise(\"No plugin #{plugin} defined\") unless @plugins.include?(plugin)\r\n\r\n       # Create an instance of the class if one hasn't been done before\r\n       if @plugins[plugin][:instance] == nil\r\n            begin\r\n                 klass = @plugins[plugin][:class]\r\n                 @plugins[plugin][:instance] = eval(\"#{klass}.new\")\r\n            rescue Exception => e\r\n                raise(\"Could not create instance of plugin #{plugin}: #{e}\")\r\n            end\r\n       end\r\n\r\n       @plugins[plugin][:instance]\r\n   end\r\nend\r\n\r\nclass Plugin\r\n   def self.inherited(klass)\r\n      PluginManager << {:type => \"facts_plugin\", :class => klass.to_s}\r\n   end\r\nend\r\n\r\nclass FooPlugin\n

<\/code><\/p>\n

For mcollective I only ever allow one of a specific type of plugin so the code is a bit specific in that regard.<\/p>\n

I think late creating the plugin instances is quite an improvement too since often you're loading in plugins that you just don't need like client apps would probably not need a few of the stuff I load in and creating instances is just a waste.<\/p>\n

I am not 100% sold on this approach as the right one, I think I'll probably refine it more and would love to hear what other people have done. <\/p>\n

This has though removed a whole chunk of grim code from mcollective since I now store all plugins and agents in here and just fetch them as needed. So already this is an improvement to what I had before so I guess it works well and should be easier to refactor for improvements now.<\/p>\n","protected":false},"excerpt":{"rendered":"

Most of the applications I write in Ruby are some kind of Framework, ruby-pdns takes plugins, mcollective takes plugins, my nagios notification bot takes plugins etc, yet I have not yet figured out a decent approach to handling plugins. Google suggests many options, the most suggested one is something along these lines. class Plugin def […]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","footnotes":""},"categories":[1],"tags":[121,78,13],"_links":{"self":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/1186"}],"collection":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/comments?post=1186"}],"version-history":[{"count":9,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/1186\/revisions"}],"predecessor-version":[{"id":1716,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/1186\/revisions\/1716"}],"wp:attachment":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/media?parent=1186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/categories?post=1186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/tags?post=1186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}