Understanding Rails PluginsPosted: April 6, 2010
It can be hard to write plugins for Ruby on Rails. Here is a “simple” example to extend ActiveRecord (slightly modified), from the official Ruby on Rails site:
module Yaffle def self.included(base) base.send :extend, ClassMethods end module ClassMethods def acts_as_something send :include, InstanceMethods end end module InstanceMethods def to_yaffle "You are a Yaffle!" end end end ActiveRecord::Base.send :include, Yaffle
This code is downright baffling, if, like me, you’re not intimately comfortably with the world of Ruby metaprogramming. It’s easy enough to copy and paste your methods under ClassMethods and InstanceMethods, as the tutorial suggests, and hope things work, but it’s hard to understand what’s going on here. And I like to understand these things.
For example, are the module names ClassMethods and InstanceMethods special to Ruby, or are they arbitrary? It’s hard to tell.
And what the heck does this code mean?
def self.included(base) base.send :extend, ClassMethods end
What is “base” referring to here? What is “extend” (as opposed to “include”)? This code was a complete mystery to me.
The first step was to figure out that last question – what “extend” meant. I found this very useful writeup by John Nunemaker explaining the difference. Essentially, when you “include” a module in a class, the objects created by that class have all the methods of that module (instance methods). When you use “extend” instead, the methods apply to the class (class methods).
And we can see the last line is forcing ActiveRecord::Base to “include” the module:
ActiveRecord::Base.send :include, Yaffle
So far so good.
At this point, any methods in Yaffle should be added to the class ActiveRecord::Base. But there’s a caveat: when you package your methods in submodules, however, none of those methods are included or extended or whatever. They are ignored (to the best of my knowledge).
It turns out that there is a hook you can use when you include a module in a class – you can define an “included” method in the module that is called when a class actually includes the module. It is roughly akin to a constructor for the module.
So after all that, this is how the plugin above works: The first thing that happens is that you include the module in ActiveRecord::Base. That action does one thing – it triggers the “included” method of the module. That then calls extend on ActiveRecord::Base, this time directly targeting the submodule “ClassMethods” which does contain methods. So at this point all your class methods are loaded up into ActiveRecord::Base.
What about the instance methods? In most cases, you want to control which subclasses of ActiveRecord::Base include the instance methods. So you let the user explicitly call a special class method (in this case, “acts_as_something”) that has the class call “include” on the InstanceMethods submodule, which in turn provides the instance methods for objects made from that class.
Anyway, this seems to me to be overly complicated. Calling “include” in order to indirectly call “extend” doesn’t make much sense to me. This code works perfectly well:
module Yaffle #class methods def acts_as_yaffle send :include, InstanceMethods end module InstanceMethods def to_yaffle "You are a Yaffle!" end end end ActiveRecord::Base.send :extend, Yaffle
Much more concise, in my opinion. If you don’t like the lack of symmetry between the class methods and the instance methods (*cough* OCD), you can always package the class methods in a submodule as before, and change the last line to
ActiveRecord::Base.send :extend, Yaffle::ClassMethods. Same thing, and just as easy to understand.
Whew, that’s all for now! Long post – I hope it helps someone :-)
p.s. I know that gems are replacing plugins, but the issues are exactly the same.