September 19, 2009 by alex
Yesterday I added a new feature to Couch Potato, my persistence layer for CouchDB that I call Dynamically Inherited Methods™ and found the implementation I came up with interesting enough to share. Here’s the problem:
class User
def self.string_attr_reader(name)
define_method name do
instance_variable_get("@#{name}").to_s
end
end
string_attr_reader :nickname
attr_writer :nickname
end
user = User.new
user.nickname = 123
user.nickname # => '123'
This code adds a macro to the class user called string_attr_reader. When you call this macro on the class and pass it a name it generates a method that returns the instance of the same name converted to a string. This all works perfectly until you want to override the generated method to add some behavior:
class User
def nickname
super.gsub('6', '7')
end
end
The enhanced method now replaces all occurrences of 6 with a 7 - except it doesn’t. When you run the above code you will get an exception where Ruby complains there is no super to call. That’s because we defined the nickname method in the User class so we didn’t inherit it, hence no super.
The one way out of this is to use alias_method, or alias_method_chain when you are using Rails:
class User
def nickname_with_six_replaced
nickname_without_six_replaced.gsub('6', '7')
end
alias_method_chain :nickname, :six_replaced
end
First of all this isn’t very beautiful anymore, and secondly using alias_method_chain when you could use standard object oriented metaphors (like inheritance) doesn’t make a lot of sense - except that you can call yourself a metaprogramming programmer, yay.
Anyway, there is another way - actually involving much funkier meta programming - to solve the problem:
class User
def self.string_attr_reader(name)
accessor_module.module_eval do
define_method name do
instance_variable_get("@#{name}").to_s
end
end
end
private
def self.accessor_module
unless const_defined?('AccessorMethods')
accessor_module = const_set('AccessorMethods', Module.new)
include accessor_module
end
const_get('AccessorMethods')
end
end
The updated code now dynamically creates a Module called User::AccessorMethods and includes it into the User class. All methods generated by string_attr_reader are now added to that new module instead of the class. The result is that when overriding those methods you can now call super because they’re are inherited from the new module.
class User
attr_writer :nickname
string_attr_reader :nickname
def nickname
super.gsub('6', '7')
end
end
user = User.new
user.nickname = 678
user.nickname # => '778'
While this code involves some fairly crazy metaprogramming which would be too much for a simple example like the above, I think libraries like CouchPotato can still benefit, as the application code can become cleaner by not having to do any metaprogramming but resort to standard object oriented ways of programming.