Last time I discussed Ruby and metaprogramming, I was trying to stay DRY (Don’t Repeat Yourself) while coding some very similar-looking methods. The solution, then was to use class_eval to dynamically add methods into the current class, the way attr_accessor and it’s peers already do.
Today I was having a similar, yet more difficult problem: the architecture of my app includes two controllers: IncomeController and ExpenseController, two identical twins, each saving, updating and listing it’s associated models (Income/Expense), where Income has_many income_attributes and.. you guessed it, Expense has_many expense_attributes. And the list can go on, since they have some more relationships, all of them being identical but for the names and associations.
I know, perhaps such thing might have been avoided if, in the first place, we would have used single-table inheritance and such. But for various reasons we didn’t, and I was getting tired of keeping in sync all the changes in Income* to Expense* and vice-versa.
So.. what could the solution be? Namely, I wanted both IncomeController and ExpenseController to share the same codebase, the only difference being the names; I wanted a… replace_all to be run on the fly.
Luckily for me, I remembered Camping, a Ruby Microframework for web apps(kinda like Rails, but more simplistic), which uses some intense metaprogramming-kung-fu to keep its code to less than 4kb. For instance, in the homepage example, we see that
A skeletal Camping blog could look like this:
The .goes method returns a class duplicate of the Camping one, where all occurences of Camping have been changed with Blog.
The code I use for my metaprogramming needs is quite similar to the one of Camping, adding just a bit more readability and a couple of changes to make things work in my app:
I created a IncomeExpenseController class in income_expense_controller.rb, containing all the common functionality; I made sure to replace_all occurences of Income by IncomeExpense; same for the lowecase version.
Then, in income_controller.rb I have only this:
Similar, in expense_controller.rb I have only this:
In the income_expense_controller.rb file, within the body for IncomeExpenseController class, I added this:
#start metaprogramming S = IO.read(__FILE__) rescue nil NAME = "IncomeExpense" class << self def goes(m) myS = S.gsub(Regexp.new(NAME), m.to_s) myT = myS.gsub(Regexp.new(NAME.underscore), m.to_s.downcase) eval myT, TOPLEVEL_BINDING end end # end metaprogramming
I actually tried to put this #metaprogramming part in a separate module, but for some unknown reason it wouldn’t work that way. If anyone succeeds in making it work within a separate module, I’d like to know how.
Note that the String.underscore method is part of ActiveSupport, so it’ll work on Rails. Also note that I added this metaprogramming DRY fragment in quite a lot of my files: the Income/Expense models, the IncomeAttribute/ExpenseAttribute models and many more.
I love it!