Sothr's Corner

Modular Plugins and Ruby

There have been (at least a few times) that during the course of developing a personal project, that I’ve considered using a plugin style system to solve a particular problem. However, I have never designed such a system and almost always find that what I wanted to do can be solved in another way that is far simpler. A few days ago I discovered a real use case for such a pattern and finally bit the bullet and dived into discovering how I could go about implementing such a pattern. Because the project was in Ruby, I hoped that the solution would be simpler than if I tried to implement it in a stricter language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Class that will dynamically be altered to reference our plugins
class PluginConfiguration
    # A static list of loaded plugins
    @@loaded_plugins {}
    def get_loaded_plugins
        # Allow access to loaded plugins from instances
        @@loadedPlugins
    end

    # Simple not_implemented method for default example
    def not_implemented
        raise "Not Implemented"
    end

    # "load" the default into the listing so we can call it dynamically
    @@loaded_plugins['default'] = :not_implemented
end

The above code is the guts of the plugin system I’ve been developing. Each plugin system will need an appropriately named plugin configuration object to store information about the loaded plugins and also serve as the object that is modified with the contents of our plugins.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# The interface to the plugin system
class PluginManager
    attr_reader :default_plugin
    def initialize(default_plugin = 'default')
        @default_plugin = default_plugin
        @config = PluginConfiguration.new
    end

    # Allow access to the list of plugins that are recognized as loaded
    def get_loaded_plugins
        @config.get_loaded_plugins
    end

    # Interface to call for the plugin to act on some sort of data
    def process(plugin_name, data)
        # Reference to what method will by handling our request
        plugin_handler = nil
        # Check to see if the requested plugin is loaded and if so, use it
        if get_loaded_plugins.include? plugin_name
            plugin_handler = @config.method(get_loaded_plugins[plugin_name])
        # Otherwise use the default
        else
           plugin_handler = @config.method(get_loaded_plugins[@default_plugin])
        end
        # Call the handler and return the result
        plugin_handler.call(data)
    end

    # Dynamically call for the loading of all plugins in the ./lib/plugins directory
    Dir.glob('./lib/plugins/*.rb').each{|f| require f}
end

This is the guts of the plugin handler and acts as our interface to calling loaded plugins. The “process” method is used to route the request to the loaded plugin or gracefully fall back onto the default handler which can do whatever we want. In this case I simply have it raising an error that what we’re trying to do is not implemented.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Module that will be the basis for our processing
module NewPlugin
    # Method that we'll register to act for our plugin
    # It's a good idea to prevent the stepping on other plugins to name the methods according to the plugin name
    def new_plugin_process(data)
        # Do the work as the plugin
        puts "Called new processor plugin"
    end
end

# Overload the PluginConfiguration class to include our new plugin
class PluginConfiguration
    include NewPlugin
    # Set the plugin in our registry so that we can call it dynamically by the name set
    @@loaded_plugins['new'] = :new_plugin_process
end

This is the implementation of a simple plugin that will, when loaded, insert itself as a module in PluginConfiguration and then register as a callable method in the loaded_plugin hash. The biggest issue here is to make sure we won’t step on any other loaded plugins.

The idea behind this system is that we use mixins to dynamically increase the functionality of the PluginConfiguration class, and then we register that functionality into the hash so that it can be dynamically called. This way we have runtime dynamic methods without having to define them in the code. My use case that led to this design was a dynamic parser that can be configured to parse a source differently based on the expected content. For example an RSS feed can have a special RSS parser, and Atom feed can have an Atom parser, raw HTML can have it’s own parser, and any other source we can dream of can have a parser. As I work, I can write the parsers independently without having to update the plugin manager with the list of loaded parsers.

I’m sure there are plenty of other ways to do this in Ruby, and my way is probably not the best. But it is working wonderfully for what I need it to do.

Comments