Using method_missing in Ruby
I first learned about methodmissing when <a href="http://devlicio.us/blogs/christopherbennage/">Christopher Bennage asked me about it. Even after looking it up and explaining it to Christopher, I didn't find any practical use for it in the things I was doing.
At least until tonight, that is.
I've been working on expanding Hair Forecast into Canada. I have the Canadian weather data described in active record like so:
createtable :canadaforecasts do |t| t.column :fcstdate, :date t.column :lat, :float t.column :lon, :float end<!--break-->
createtable :canadaparameters do |t| t.column :canadaforecast_id, :integer t.column :parameter, :string t.column :interval, :int t.column :value, :float end
As you can imagine from that, a canadaforecast has_many
canadaparameters. Each parameter is one of T for temperature, D for dewpoint, W for wind, etc. Each parameter has an interval, which is described in hours past 0:00 UTC. This is so I can have each weather parameter for morning, day, night, etc.
Now here's the interesting part. When I'm retrieving a forecast, and wish to access temperature for the 12th hour of the period, I would end up with this:
forecast = Canadaforecast.find(somecriteria) forecast.canadaparameters.findbyparameterand_interval("T", 12)That second line just ain't that pretty. Ruby is supposed to be pretty. So I decided to simplify it.
I could create a fetch method in the Canadaforecast model that simply takes two args (parameter and interval) and returns the value. So then I'd have a bunch of lines like:
t = forecast.fetch("T", 12) d = forecast.fetch("D", 12)But what if I thought the following was more readable and just plain looked cool?
t = forecast.T(12) d = forecast.D(12)Well, methodmissing is the tool for this. Consider the following model in my Rails app:
class Canadaforecast < ActiveRecord::Base hasmany :canadaparameters
Valid_parameters = [ "T", "D", "C", "W", "P", "H" ]
def methodmissing(method, *args) if Validparameters.include?(method.tos) fetch(method.tos, args) else super end end
private def fetch(parametername, interval) parameter = self.canadaparameters.findbyparameterandinterval(parametername, interval) if parameter.nil? nil else parameter.value end end end The methodmissing method checks to see if you are trying one of the valid forecast parameters as a method. If not it just sends you back to the super and throws an error. If you have used a valid parameter as a method, then it calls the simple fetch method and passes in the parameter and interval you are trying to get. Note how methodmissing provides an array of args.
Now I can fetch any forecast parameter for any interval with very easy to read code.
No model would be complete without a proper test, so just to show off my (lack of) testing skills, here's the test for the model above:
require File.dirname(FILE) + '/../test_helper'
class CanadaForecastTest < Test::Unit::TestCase fixtures :canadaforecasts, :canadaparameters
def testfetchingparametersbyusingparameternameasmethod forecast = Canadaforecast.find(1) assertnotnil forecast
value = forecast.H(12) assert_equal 10, value end
def testfetchingparametersnotfound forecast = Canadaforecast.find(1) assertnotnil forecast
value = forecast.H(1) assert_nil value end end