Really simple and naïve Ruby plugin framework

I recently found myself writing some Ruby (IronRuby to be specific) at work for Umbraco that needed to generate HTML for different types of content, destined to be displayed in an aside column. There are many ways of doing something like this, but in order to not violate the Open/Closed Principle, I decided to create a very simple and naïve plugin framework that would automagically wire up new content-type handlers. The way I chose to implement this, was to leverage the meta-programming capabilities in Ruby, more specifically the inherited hook.

Say hello to your friendly neighbourhood hooks

One of the most powerful aspects of Ruby, is its meta-programming capabilities; the aspect that allows you to shape the language to fit your needs and requirements - at runtime.

Hooks are perhaps not strictly speaking a meta-programming capability, but they play a very important role in meta-programming. Undoubtedly the most infamous of all the hooks is method_missing. As the name suggest, it’s the hook that allows you to intercept calls to undefined methods.

class Hello
  def method_missing(name, *args)
    "Hello #{name.capitalize}!"
  end
end

hello = Hello.new
puts hello.neighbour  # "Hello Neighbour!"

Respect the hook

To implement the plugin framework, I decided to leverage a seemingly under-appreciated hook found on Class called inherited. Yet again, the name gives it all away - this hook is called whenever the class is implemented (sub-classed.) We can utilise this hook to implement a simple plugin registration system packaged up in a module.

module Plugin
  module ClassMethods
    def repository
      @repository ||= []
    end

    def inherited(klass)
      repository << klass
    end
  end

  def self.included(klass)
    klass.extend ClassMethods  # Somewhat controversial
  end
end

Because we want to add singleton methods to whatever class includes our plugin module, we use a common, albeit slightly controversial technique; leverage another hook - #included - to automagically extend the target class with the our ClassMethods modules.

Let’s build some plugins!

With the Plugin module we have the foundation needed to implement various types of plugins; let’s create a very silly plugin type for displaying various kinds of messages.

# ./lib/message_plugin.rb
require './lib/plugin'

class MessagePlugin
  include Plugin

  def display_output
    raise NotImplementedError.new('OH NOES!')
  end
end

# ./plugins/hello_world.rb
class HelloWorld < MessagePlugin
  def display_output
    puts 'Hello World! :-)'
  end
end

# ./plugins/goodbye_world.rb
class GoodbyeWorld < MessagePlugin
  def display_output
    puts 'Goodbye World... :-('
  end
end

Because we’ve taken advantage of the inherited hook, all that is necessary for plugins of type MessagePlugin to work, is to require the files containing the implementations, e.g. a directory called “plugins.”

dir = './plugins'
$LOAD_PATH.unshift(dir)
Dir[File.join(dir, '*.rb')].each {|file| require File.basename(file) }

Now all that is required for someone to add a new message to our application, is to inherit from MessagePlugin and drop the implementation into the “plugins” folder.

Taking it one step further…

The MessagePlugin is extremely simple - what if we want to pass data to a plugin? Perhaps we only want to pass the data to plugins that can handle that type of data. An easy way of pulling this off, is to query the registered plugins on whether they can handle it.

# ./lib/type_handler_plugin.rb
require './lib/plugin'

class TypeHandlerPlugin
  include Plugin

  def self.for_type(type)
    repository.find {|handler| handler.can_handle? type }
  end
end

# ./plugins/string_handler.rb
class StringHandler < TypeHandlerPlugin
  def self.can_handle?(type)
    type == String
  end

  def display_output(data)
    puts "String: #{data}"
  end
end

# ./plugins/time_handler.rb
class TimeHandler < TypeHandlerPlugin
  def self.can_handle?(type)
    type == Time
  end

  def display_output(data)
    puts "Formatted Time: #{data.strftime '%A, %B %m, %Y'}"
  end
end

The #can_handle?(type) predicate method hands the responsibility over to the plugins, thus requiring no changes to any other class whenever we add a new type and/or type handler to the application. The Open/Closed Principle remains unviolated.

One more thing…

Just for good measure, here is a rather silly test harness for running the various plugins we’ve peeked at - enjoy responsibly!

require './lib/array'
require './lib/message_plugin'
require './lib/type_handler_plugin'

#
# Add plugins folder to LOAD_PATH and subsequently require all plugins.
#
dir = './plugins'
$LOAD_PATH.unshift(dir)
Dir[File.join(dir, '*.rb')].each {|file| require File.basename(file) }

class TestHarness
  def self.run
    run_message_plugin
    run_type_handler_plugin
  end

  private

    def self.run_message_plugin
      message_plugin = MessagePlugin.repository.random.new
      message_plugin.display_output
    end

    def self.run_type_handler_plugin
      random_data = [Time.now, 'Example string.', 1337].random
      type_handler_plugin = TypeHandlerPlugin.for_type(random_data.class)
      unless type_handler_plugin.nil?
        type_handler_plugin = type_handler_plugin.new
        type_handler_plugin.display_output(random_data)
      end
    end
end

TestHarness.run