An useful metaprogramming spell I recently played with is the
Module#define_method()
, which dynamically adds an instance method to the class on which is called.
I found it particularly useful to add data accessor methods on “static” Rails data model: suppose I’m working an e-commerce Rails webapp, and I have a
Country
model which maps the countries suitable for shipping, or a
PaymentType
model which represents all the possible payment types.
For these kind of models (and tables), which are typically static (they don’t change often), you often have to access specific values, say
Country.italy
or
PaymentType.credit_card
.
In these cases, defining dynamically an accessor method may be useful and more clear than always perform a
find_by_name("my value")
.
So, for example, I open up my country.rb model class and add these lines
[sourcecode language=”ruby”]
class << self
Country.all.each do |each_country|
define_method(each_country.name.downcase.gsub(‘.’, ”).gsub(’ ‘, ‘_’)) do
Country.find_by_iso_code(each_country.iso_code)
end
end
end
[/sourcecode]
And then opening the Rails console I will be able to type
1.8.7@epistore > Country.sri_lanka
# {
:id => 59,
:zone => "U9",
:enabled => true,
:created_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
:updated_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
:iso_code => "LK",
:country_set_id => nil
}
Just a note: as I said,
Module#define_method()
will add an
instance method on the class. To add a
class method, which is what I want, we have to use a different approach, using the
class << self
syntax to add a singleton method in the receiver.
I may also add a query method on each
Country
instance to check that country against another country (for example, I may ask
my_country.italy?
)
Country.all.each do |each_country|
define_method(each_country.name.downcase.gsub('.', '').gsub(' ', '_').concat('?')) do
has_iso_code? each_country.iso_code
end
end
And then, after issuing a
reload!
command in the Rails console, I may type:
1.8.7@epistore > Country.usa.usa?
true
1.8.7@epistore > Country.usa.italy?
false
1.8.7@epistore > Country.usa.south_korea?
false
1.8.7@epistore > Country.south_korea.south_korea?
true
Depending on the kind of Rails app you have, these may be a useful tip.