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
How about
@course.holes[18]?Funny, I just came across Jamis Buck’s finder shortcut (again).
That’s perfect, though—thanks.