Delegation and Demeter in Rails

Friday, December 22

I recently discovered this little nugget. After reading Jay Fields’ post on the topic of decoupling and the law of Demeter I’ve been using Ruby’s Forwardable module to accomplish basic delegation tasks. Alas, Rails (via ActiveSupport) has a really handy method that encapsulates this pattern quite nicely: a class method called delegate

Consider a Person class with an associated Profile. Pretend the Profile class has an associated collection called favorite_books.


class Person < ActiveRecord::Base
  has_one :profile
end

class Profile < ActiveRecord::Base
  belongs_to :person
  has_many :favorite_books
end

If you wanted to see a Person’s favorite books collection, you’d have to go through profile:

Person.find(:first).profile.favorite_books

Which of course would throw a no method error if profile returned Nil. Using delegation, we can do this:


class Person < ActiveRecord::Base
  has_one :profile
  delegate :favorite_books, :to => :profile
end

Now we can say:

Person.find(:first).favorite_books

and not have to worry whether the profile exists or not, thus alleviating us from having to write an accessor in Person like def favorite_books() profile ? profile.books : nil; end.

Very nice indeed.

Comments

Leave a response

  1. Tyler PreteJanuary 02, 2007 @ 09:34 PM

    Hello there. This article was very informative, and I am already using it in my latest app. I have two questions however, and I haven’t been able to find the answers by googling. Perhaps you know. Is there anyway to specify the name you want the delegate to work with?

    To go with your example, say I wanted the profile’s id. Is there anyway to get Person.find(:first).profile_id and have that go through the book and get the id?

    I was thinking something like delegate :profile_id, :to => profile, :as => :id

    or something of that nature.

    Also, is there any way to do double delegation? aka Person.find(:first).favorite_chapter With delegation in Person.rb: delegate :favorite_chapter, :to => profile and in Book.rb delegate :favorite_chapter, :to => book

    So that you can pass it all the way through?

    If you have any answers either way, or can even point me in the right direction, that would be great.

    Thanks, Tyler Prete

  2. David RichardsJanuary 19, 2007 @ 04:59 AM

    Am I missing something here? I just recreated this example in a fresh rails app. The system delegates as expected if the parts are in place, but doesn’t return an empty set if the linking model is missing. E.g., if I create a Person without a Profile, I get the no method error. I have to have a profile in place in order to have the delegator work for me.

    In other words, I can shorten the syntax from Person.find(1).profile.favorite_books to Person.find(1).favorite_books, but I don’t have anything that’s going to just return a nil if there’s anything missing in the link of models.

    I.e., I’m still error checking in my finder methods, or writing things like def favorite_books() profile ? profile.books : nil; end

  3. Akhil BansalJanuary 24, 2007 @ 01:50 AM

    Wow, This is really useful. I am really going to use this ;-)

  4. PackagethiefJanuary 28, 2007 @ 03:29 PM

    David—it would appear that you are right. I guess what I really wrote about was how I expected this feature to behave. I should have proved my assumptions before posting. A quick look at its source indicates that it would be easy add the desired behavior. I smell a patch…