post

How to use method_missing
How to use method_missing

There’s no doubt, Ruby can be tricky at times. But understanding it well (and not that I do) can greatly increase productivity, efficiency, and happiness. Seriously.

Would you believe me if I said I could randomly call Model.ryan_heath and dynamically teach Ruby that ryan_heath is a real method under the Model class? No, Ruby doesn’t come pre-packaged with a method that just-so-happens to be my name. There’d be no trick in that. Rather, this works because of method_missing. If you’ve never heard of method_missing, it’s simply a method that gets invoked after Ruby searches the entire class hierarchy for a method that doesn’t exist (Model.ryan_heath would result in Object.method_missing). So what, right? Wrong. Since methods can be easily overridden in Ruby, you can use this little hook to really help you write (or not write) code.

As an example (albeit, a shallow one) to show how flat out awesome this technique is, I’m going to use a snippet from Golf Trac (which is taking me forever to finish, by the way). So, in my golf application I have the obvious association that “a course has many holes”. Since I’ll be referencing those holes a good bit throughout the application, it would be nice to have a convenient way to access them. I’ll rarely (if at all) be asking for hole information outside the context of a course, and so it’d be best to define a method directly on the course-to-holes association.

While this can simply be any method I’d like, I chose to override method_missing (you’ll see my original intention in a minute). Here’s the top of my Course model:

class Course < ActiveRecord::Base
  has_many :rounds, :include => :scores
  has_many :holes do
    def method_missing(method)
      find(
        :first, 
        :conditions => { :hole_number => method.to_s.send(:gsub, /[a-z|A-Z|_]/, "").to_i }
      )
    end
  end
  ...
end

Here, find will always be scoped to a specific course, so I only need to worry about getting the appropriate hole. Now, to access the holes:

@course.holes.hole_10
# => tenth hole
@course.holes.hole_18
# => eighteenth hole

The best part is, the hole_10 and hole_18 methods don’t really exist! How cool is that?

Also, looping through all of the holes using the above implementation may seem to make sense, but it doesn’t. If I were to use the above jargon, I’d be doing a separate query for each hole in the loop. In that case, I’d probably rather eager load the holes instead. However, if I didn’t care to run 18 separate queries, it could be done like so:

(1..18).each do |i|
  puts "Par for the #{i.ordinalize} hole is #{@course.holes.send("hole_#{i}".to_sym).par}."
end

## results
 # => "Par for the 1st hole is 4."
 # => "Par for the 2nd hole is 3."
 # => ...

I’ve essentially told Ruby that there are 18 methods (hole_1, hole_2, ...hole_18) defined on the course-to-holes association, when really there’s nothing more than a single method_missing hook. That’s just awesome to me.

And now it’s time for my original intention. Originally, I had planned on using the hole number itself as the method, which would have been easier and much cleaner. But I can’t seem to get it to work. I’m currently using course.holes._10 instead of the more redundant, course.holes.hole_10, but what I really want is just course.holes.10.

def method_missing(method)
  find(:first, :conditions => { :hole_number => method.to_i })
end

@course.holes.18
# => eighteenth hole
@course.holes.11
# => eleventh hole

But, and correct me if I’m wrong, it’s apparent that Ruby doesn’t quite understand integers as method calls. Oh well, Ruby does so many things that amaze me, it was worth a shot. If anyone has a thought or two as to how I can get course.holes.18 to return the 18th hole, I’d love to hear them.

I still have a lot, a lot, to learn about metaprogramming, but it definitely feels good to have your code do extra work for you. If you have a clever method_missing example, feel free to leave it in the comments.

Comments
01
05 Aug 2007 10:56 PM

How about @course.holes[18] ?

class Course < ActiveRecord::Base
  def [](hole)
    # finder call
  end
end
02
05 Aug 2007 11:12 PM

Funny, I just came across Jamis Buck’s finder shortcut (again).

That’s perfect, though—thanks.




Please rewrite the image text in the SPAM field: Spam Protection

Preview

2008 by Ryan Heath | Get In Touch

flickr

DesolateInfinityLooking upDazedBlurred