Understanding Ruby and Rails: Delegate

December 15th, 2009 at 7:42 pm • permalink11 comments

This article targets Rails 2.3 and Rails 3. The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

Delegation is a quite common practice in Ruby projects, if you consider proxies, mixins and composition as the ingredient of the Delegation Pattern.

The Delegation Design Pattern is a technique where an object exposes certain behavior but it actually delegates responsibility for implementing that behavior to an associated object.

A really common technique is to use method_missing to intercepts the calls to undefined method, then forward the call to the right handler. However, this isn’t always a good idea. There are better ways to implement the delegation pattern in Ruby.

The Ruby standard library contains a Delegate module that aims to provide support for the Delegation pattern. Sadly, I found it to be way more complex than the traditional approach and I never really used it.

ActiveSupport Delegate module

If your project includes ActiveSupport, and every Rails project does, you have a more clean and easy way to implement the delegation pattern: the Module#delegate extension. It provides a delegate module you can use in your class or in your modules to delegate a specific method to an associate object.

For instance, consider a standard Post model which belongs to a User.

class Post
  belongs_to :user
end

class User
  has_many :posts
end

You might want a call to post.name to return the name of the user associated to the given post. Normally, you would create a new name method as follows

class Post
  belongs_to :user

  def name
    # let's use try to bypass nil-check
    user.try(:name)
  end
end

The same code expressed using the delegate method.

class Post
  belongs_to :user
  delegate :name, :to => :user, :allow_nil => true
end

The delegate method can be used in any context, it’s not limited to ActiveRecord models. For example, your custom queue wrapper can delegate to the internal queue implementation some specific methods.

class QueueManager

  attr_accessor :queue

  # Delegates some methods to the internal queue
  delegate :size, :clear, :to => :queue

  def intialize
    self.queue = []
  end

end

m = QueueManager.new
m.size
# => 0
m.clear
# => []

Methods can be delegated to instance variables, class variables, or constants by providing them as a symbol. At least one method and the :to option are required.

Options

The delegate method understand some additional options, useful to customize the behavior.

This is my favorite option. The :prefix can be set to true to prefix the delegate method with the name of the object being delegated to. You can also provide a custom prefix.

class Post
  belongs_to :user

  delegate :name, :to => :user, :prefix => true
  # post.user_name

  delegate :name, :to => :user, :prefix => "author"
  # post.author_name
end

The :allow_nil option allows the class to delegate the method to an object that might be nil. In this case, a call to the delegated method will return nil. The default behavior is to raise a NoMethodError.

class Post
  belongs_to :user
  delegate :name, :to => :user, :prefix => true
end

Post.new.user_name
# raise NoMethodError

class Post
  belongs_to :user
  delegate :name, :to => :user, :prefix => true, :allow_nil => true
end

Post.new.user_name
# => nil

The :to it a non-option, because it’s mandatory.

Documentation

The ActiveSupport delegate extension has a detailed documentation but, unfortunately, it doesn’t show up in the main Rails documentation nor you can find it in the ActiveSupport documentation. I suggest you to jump directly into the source code, it is worth the effort.

Filed in Programming • Tags: , , ,

Comments

brainopia says:

Don’t forget to mention forwardable.

Kally says:

I’m probably wrong, but I feel like this is more useful with non-activerecord classes, although most people think the exact opposite.
For example, if I want to write
Post.new.user_name, it makes more sense to me to write Post.new.user.name, same number of characters, but it is more clear what’s happening, without any quasi-virtual attribute.
Interesting anyway the technique in general.
Thanks.

Glenn R says:

Hi Kally

I agree that the ‘Post.new.user.name’ style of doing things can be more natural and readable, but it may be a sign of coupling in your app.

There’s a good discussion of it here: http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx

I try to always keep in mind that those long chained method calls are a bit of a code smell, and to consider a refactor with delegate() if it makes sense.

[...] For the most part of Ruby code, this change won’t mean anything special. However the BasicObject plays a fundamental role to implement the proxy pattern and dynamic delegation. [...]

[...] you take what you can get. If you want more readable code, check out ruby delegation library and Rails ActiveRecord delegation. So, let’s put this all together to see the real power of define_method. This is adapted [...]

rubyu2 says:

i agree that the ‘post.new.user.name’ style.
but sometime i will write a method like that ‘def user_name self.user.name end’ if use user_name offen.

fearless fool says:

I’m a little late to the party, but I don’t understand how the first example of delegation works:

class Post
belongs_to :user
delegate :author, :to => :user, :allow_nil => true
end

Either there’s a typo, or User magically knows that :author really means to invoke the :name method, or I’m missing something fundamental. I’m hoping its the former. Clarification?

You’re right, there’s a typo. To keep it simple, I changed the first example to assume the method to be invoked is called author.
Thanks for pointing it out. :)

bra1n says:

def ma,e
# let’s use try to bypass nil-check
user.try(:name)
end

seems like a typo, too…? Or did you indeed mean “ma,e” instead of “name”?

Yan Cherfan says:

Very useful post. Thanks!

I just ran across this functionality while source diving in Rails. I’ve implemented this in a more manual way many times. Now that I know about delegate, I’m going to be using it a lot more often. Thanks!

Add a Comment




Follow Me
    Random Quote