zerosum dirt(nap)

evolution through a series of accidents

zerosum dirt(nap)

ActiveRecord Delegation Pitfalls

May 30, 2007 by nap · Comments

Delegation is a powerful concept. And it’s a useful Ruby mixin, too. You can use the delegation mixin to easily expose related objects’ methods as your own, which makes for much cleaner code than defining those methods by hand. However, there are some pitfalls to be aware of when dealing with delegation and the ActiveRecord lifecycle.

Here’s a simple example of delegation in action:

class User < ActiveRecord::Base
  has_one :homepage, :class_name => "Article"
  delegate :url, :to => :homepage
end

my_user.url => url of the home page article for this user

Yes, we could just call my_user.homepage.url here too. But there are definitely some situations where you’d prefer not to do this. I won’t attempt to relate my present situation to yours, as it’s outside the scope of this discussion. Anyway, the point is that delegation can be used to make your life easier.

Here’s where we get into trouble:

new_user = User.new
new_user.url

NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url

O NOES! What happened?

In a nutshell, when we use it in our model, the delegate class method defines a bunch of new instance methods on our User object. These methods simply forward their queries onto the target (:to) object. If the target object, at the present stage in the AR model lifecycle, happens to be nil, then we’re trying to call a method on nil. The nil object doesn’t have a url method, so we’re toast.

def #{method}(*args, &block)
  #{to}.__send__(#{method.inspect}, *args, &block)
end

How can we get around this? Well, court3nay opened a ticket to address this a while back. It was recently closed due to inactivity. I just reopened it, and added a small patch. Here’s the difference in the way the mixin creates the delegated methods:

-  #{to}.__send__(#{method.inspect}, *args, &block)
+  #{to}.__send__("nil?") ? nil : #{to}.__send__(#{method.inspect}, *args, &block)

Pretty simple really. We just test the delegation target to see if it’s nil first. If it is, we return nil instead of trying to send it a message. Otherwise, we call the method on the target object and let the receiver worry about it.

Huzzah, we’ve successfully delegated the task of dealing with nil delegation targets! Way special, eh?

blog comments powered by Disqus