Tuesday, September 23, 2008

Rails plugins: Metaprogramming vs Generators

One of the most powerful features of Rails is plugins. Plugins enable developers to make generic extensions to Rails applications that others can benifit from. One could use two different approaches to add new logic/aspects to a Rails application through a plugin: metaprogramming and generators. This entry is not a tutorial about writing Rails plugins. Assuming that you know the basics of metaprogramming in Ruby, and using a very basic example, we'll try to conclude a simple comparison between the two approaches that can be generalized.

Assume that your plugin relies on adding some logic as a before_filter to all controllers. You have that logic in a module that you want to mix in ApplicationController. Also you want to declare that filter. Using the first approach, Ruby metaprogramming, you can apply virtually any changes to the existing classes, modules and even objects. The following two lines can help in our example:
ApplicationController.class_eval "include MyModule"
ApplicationController.class_eval "before_filter :my_filter"

Basically, what we have just done is that we told the interpreter to dynamically evaluate those two lines in the context of the ApplicationController. Thos two lines are NOT lexically added to the definition of the class. It's like they're added at runtime.

Another approach we could use is a Rails generator. A generator, in a nutshell, lexically adds generated code to the existing files. We could do the same job in the example using a generator as follows:


class MyGenerator < Rails::Generator::Base

def manifest
record do |m|
m.gsub_file 'app/controllers/application.rb', /(#{Regexp.escape("class ApplicationController < ActionController::Base")})/mi do |match|
"#{match}\n include MyModule\n before_filter :my_filter\n"
end
end
end

end

Basically, what we have just done is that we searched for the line class ApplicationController < ActionController::Base and LEXICALLY added the two lines next to it. Of course, this generator has to be run to apply its changes.
ruby script/generate my_generator


The tradeoff between generation and metaprogramming is simply the tradeoff between being mixed but explicit and being isolated but subtle. Generation may result in mixing some logic, but has a major advantage of being explicit. The resulting code is explicitly added to the project files, and can even be modified by the developer using the plugin. Metaprogramming forces separation of concerns, but is done in a subtle way that could lead in a time waste for the developer investigating what happened behind the scenes.

Of course, there are situations where only generation could be used, like adding migrations, routes and other stuff. Take a look at the features Rails generators give.

Rails/Generator/Commands/Base
Rails/Generator/Commands/Create
Rails/Generator/Commands/Destroy



No comments: