emphatic solutions // ephemeral musings in the ether

Dancing with dynamic features in Ruby

I was working on a “browse” page recently for a rails-based web application. The page was not all that uncommon, requiring a listing of categories with a long list of subcategories. Without getting into much detail, I’ll note that each of these top-level types are represented by different models.

A particular “bad smell” emerged when seeing some repetitious code in a controller:

mfgs = Manufacturer.find(:all, :select => "name")
@manufacturers = mfgs.map(&:name).join(", ")
@manufacturers_count = mfgs.length

archs = Architecture.find(:all, :select => "name")  
@architectures_count = archs.length  
@architectures = archs.map(&:name).join(", ")  
   
platforms = Platform.find(:all, :select => "name")  
@platforms_count = platforms.length  
@platforms = platforms.map(&:name).join(", ")

Not only does this code repeat the same pattern for each “type”, but there were likely to be more types that needed to be added in.

Time for some refactoring. “Now if only there was a way to dynamically add instance variables to an object…”

First, lets get a simple array of our types:

types = ["Manufacturer", "Architecture", "Platform"]

For each type, we’ll get a handle on the class with that name:

types.each do |type| model = Object.const_get(type)

We’ll execute our ActiveRecord statement

listing = model.find(:all, :select =>"name")

Get an array of the “name” property

names = listing.map(&:name);

Now, we’ll create those instance variables on the object, the first being a comma-separated listing of the names:

self.instance_variable_set("@" + type, names.join(", "))

The second being a count of the elements:

self.instance_variable_set("@" + type + "_count", names.length) }

We could get into why we need those two variables (one for the list and another for the count), but I think this is a nice example of how to use instance_variable_set anyway. Ruby. Dynamic. Awesome.

The full snippet:

types = ["Manufacturer", "Architecture", "Platform"]  
types.each do |type|  
  model = Object.const_get(type)  
  listing = model.find(:all, :select =>"name")  
  type = type.downcase  
  names = listing.map(&:name);  
  self.instance_variable_set("@" + type, names.join(", "))  
  self.instance_variable_set("@" + type + "_count", names.length)  
end

Thoughts from Twitter