Saturday, May 19, 2007

Ruby: method_missing alternatives

I love the value that method_missing provides; unfortunately, sometimes I get into trouble when I abuse the power Matz has bestowed upon me. If you've done much work with method_missing the following error message probably isn't foreign to you.
SystemStackError: stack level too deep
It's a painful message to see, as it's always problematic to debug. Given the inherent difficultly of debugging method_missing calls I thought it might be valuable to offer a few alternatives.

As an example I'll use the valid_for_[a group] methods that Validatable exposes. The Validatable validations allow you to put your validations into groups.
class User
include Validatable

attr_accessor :first_name, :last_name, :ssn
validates_presence_of :first_name, :last_name, :ssn, :groups => :account_creation
validates_presence_of :ssn, :groups => :persistence
end
Given the above User class you can expect the following behavior.
irb(main):010:0> user = User.new
=> #<User:0x696900>
irb(main):011:0> user.ssn = 111223333
=> 111223333
irb(main):012:0> user.valid_for_persistence?
=> true
irb(main):013:0> user.valid_for_account_creation?
=> false
irb(main):014:0> user.first_name = 'Shane'
=> "Shane"
irb(main):015:0> user.last_name = 'Harvie'
=> "Harvie"
irb(main):016:0> user.valid_for_persistence?
=> true
irb(main):017:0> user.valid_for_account_creation?
=> true
As you can see the User instance exposes the valid_for_account_creation? and the valid_for_persistence? methods. Of course, we want the valid_for_[a group] methods to change based on the groups defined in each class. Additionally, we only want valid_for_something? to be defined on a class if that class contains any validations in the something group. This looks like a classic situation for using method_missing; however, I chose one of the following other solutions.

Delegate Responsibility:
The basic idea behind this solution alternative is to move the method_missing logic into a class whose sole responsibility is the method_missing logic. For example, had I chosen this solution for Validatable I would have made the valid methods work like the following example.
irb(main):010:0> user = User.new
=> #<User:0x696900>
irb(main):011:0> user.ssn = 111223333
=> 111223333
irb(main):012:0> user.valid_for.persistence?
=> true
irb(main):013:0> user.valid_for.account_creation?
=> false
Given the above syntax I could have created a ValidFor class that encapsulated the logic for dynamically executing a subset of validations. A few cons for this solution are the obvious violation of the Law of Demeter and the fact that the stack overflow can still exist in the delegate class. However, I like to use this solution when the methods that are necessary are truly dynamic and cannot be easily predicted (i.e. the find_by_* methods that ActiveRecord::Base exposes). Additionally, the added class removes variables when trying to figure out where the logic went wrong.

Metaprogramming:
I chose this solution because I was able to easily identify which methods were necessary and define then at interpretation time. In fact the implementation for defining the valid methods is quite simple.
def create_valid_method_for_groups(groups) #:nodoc:
groups.each do |group|
self.class_eval do
define_method "valid_for_#{group}?".to_sym do
valid_for_group?(group)
end
end
end
end
Given the above code a class will define a new valid_for_[a group] method for each group given to any of it's validations. By explicitly defining each method I get much more manageable error messages when things do go wrong.
Post a Comment