Understanding Rails Plugins

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 :-)

–Daniel

p.s. I know that gems are replacing plugins, but the issues are exactly the same.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s