Better Ruby Idioms

written

Finally, some one else (and a more reputable one), shares my Rails plugin pain. Let’s make Ruby fun again, and less magical.

Better Ruby Idioms
via Katz Got Your Tongue? by wycats on 11/12/09

Carl and I have been working on the plugins system over the past few days. As part of that process, we read through the Rails Plugin Guide. While reading through the guide, we noticed a number of idioms presented in the guide that are serious overkill for the task at hand.

I don’t blame the author of the guide; the idioms presented are roughly the same that have been used since the early days of Rails. However, looking at them brought back memories of my early days using Rails, when the code made me feel as though Ruby was full of magic incantations and ceremony to accomplish relatively simple things.

Here’s an example:

module Yaffle def self.included(base) base.send :extend, ClassMethods end   module ClassMethods # any method placed here will apply to classes, like Hickwall def acts_as_something send :include, InstanceMethods end end   module InstanceMethods # any method placed here will apply to instaces, like @hickwall end end

To begin with, the send is completely unneeded. The acts_as_something method will be run on the Class itself, giving the method access to include, a private method.

This code intended to be used as follows:

class ActiveRecord::Base include Yaffle end   class Article < ActiveRecord::Base acts_as_yaffle end

What the code does is:

  1. Register a hook so that when the module is included, the ClassMethods are extended onto the class
  2. In that module, define a method that includes the InstanceMethods
  3. So that you can say acts_as_something in your code

The crazy thing about all of this is that it’s completely reinventing the module system that Ruby already has. This would be exactly identical:

module Yaffle # any method placed here will apply to classes, like Hickwall def acts_as_something send :include, InstanceMethods end   module InstanceMethods # any method placed here will apply to instances, like @hickwall end end

To be used via:

class ActiveRecord::Base extend Yaffle end   class Article < ActiveRecord::Base acts_as_yaffle end

In a nutshell, there’s no point in overriding include to behave like extend when Ruby provides both!

To take this a bit further, you could do:

module Yaffle # any method placed here will apply to instances, like @hickwall,  # because that's how modules work! end

To be used via:

class Article < ActiveRecord::Base include Yaffle end

In effect, the initial code (override included hook to extend a method on, which then includes a module) is two layers of abstraction around a simple Ruby include!

Let’s look at a few more examples:

module Yaffle def self.included(base) base.send :extend, ClassMethods end   module ClassMethods def acts_as_yaffle(options = {}) cattr_accessor :yaffle_text_field self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s end end end   ActiveRecord::Base.send :include, Yaffle

Again, we have the idiom of overriding include to behave like extend (instead of just using extend!).

A better solution:

module Yaffle def acts_as_yaffle(options = {}) cattr_accessor :yaffle_text_field self.yaffle_text_field = options[:yaffle_text_field].to_s || "last_squawk" end end   ActiveRecord::Base.extend Yaffle

In this case, it’s appropriate to use an acts_as_yaffle, since you’re providing additional options which could not be encapsulated using the normal Ruby extend.

Another “more advanced” case:

module Yaffle def self.included(base) base.send :extend, ClassMethods end   module ClassMethods def acts_as_yaffle(options = {}) cattr_accessor :yaffle_text_field self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s send :include, InstanceMethods end end   module InstanceMethods def squawk(string) write_attribute(self.class.yaffle_text_field, string.to_squawk) end end end   ActiveRecord::Base.send :include, Yaffle

Again, we have the idiom of overriding include to pretend to be an extend, and a send where it is not needed. Identical functionality:

module Yaffle def acts_as_yaffle(options = {}) cattr_accessor :yaffle_text_field self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s include InstanceMethods end   module InstanceMethods def squawk(string) write_attribute(self.class.yaffle_text_field, string.to_squawk) end end end   ActiveRecord::Base.extend Yaffle

Of course, it is also possible to do:

module Yaffle def squawk(string) write_attribute(self.class.yaffle_text_field, string.to_squawk) end end   class ActiveRecord::Base def self.acts_as_yaffle(options = {}) cattr_accessor :yaffle_text_field self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s include Yaffle end end

Since the module is always included in ActiveRecord::Base, there is no reason that the earlier code, with its additional modules and use of extend, is superior to simply reopening the class and adding the acts_as_yaffle method directly. Now we can put the squawk method directly inside the Yaffle module, where it can be included cleanly.

It may not seem like a huge deal, but it significantly reduces the amount of apparent magic in the plugin pattern, making it more accessible for new users. Additionally, it exposes the new user to include and extend quickly, instead of making them feel as though they were magic incantations requiring the use of send and special modules named ClassMethods in order to get them to work.

To be clear, I’m not saying that these idioms aren’t sometimes needed in special, advanced cases. However, I am saying that in the most common cases, they’re huge overkill that obscures the real functionality and confuses users.