Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
DelegateClass rocks my world

If you notice that your classes have more than one responsibility, you can easily split them up into multiple, more cohesive classes using Ruby’s DelegateClass.

Let’s say that you have a Person class, and that people in your system can sell things and/or publish articles. You can’t use subclasses, because a person can be an author and a seller at the same time. At first you might start with something like this:

class Person < ActiveRecord::Base
  has_many :articles
  has_many :comments, :through => :articles
  has_many :items
  has_many :transactions

  def is_seller?
    items.present?
  end

  def amount_owed
    # => some fancy math
  end

  def is_author?
    articles.present?
  end

  def can_post_article_to_homepage?
    # => some fancy permissions
  end
end

This might seem OK at first. You might say “Well, it’s the responsibility of Person to know about both the items they’ve sold, as well as the articles they’ve published.” I say that’s hogwash.

Imagine a new requirement: People can be buyers as well as sellers / authors

The way this is setup, you’d have to re-open the person class and add things like:

class Person < ActiveRecord::Base
  #  ...
  has_many :purchased_items
  has_many :purchased_transactions

  def is_buyer?
    purchased_items.present?
  end

  # ...
end

The first thing to notice is that this violates the open / closed principle (open for extension but closed to modification) because you’ve modified the class. Next, you’ll notice that naming can get very confusing in places where you’ve got a person who is on both sides of a transaction. Finally, this code has poor separation of concerns.

Imagine another new requirement: The Person class is now driven by an xml web service, or a non-ActiveRecord class

Now that you can’t use ActiveRecord and your has_many code doesn’t work, you have to rewrite all kinds of code, and feature development grinds to a halt.

Enter DelegateClass

Let’s say instead of modifying Person, you extended Person by creating delegate classes, like so:

class Person < ActiveRecord::Base
end

class Seller < DelegateClass(Person)
  delegate :id, :to => :__getobj__

  def items
    Item.for_seller_id(id)
  end

  def transactions
    Transaction.for_seller_id(id)
  end

  def is_seller?
    items.present?
  end

  def amount_owed
    # => some fancy math
  end
end

class Author < DelegateClass(Person)
  delegate :id, :to => :__getobj__

  def articles
    Article.for_author_id(id)
  end

  def comments
    Comment.for_author_id(id)
  end

  def is_author?
    articles.present?
  end

  def can_post_article_to_homepage?
    # => some fancy permissions
  end
end

The calls to this involve one extra step, so instead of:

person = Person.find(1)
person.items

You add:

person = Person.find(1)
seller = Seller.new(person)
seller.items
seller.first_name # => calls person.first_name

Now that this is in place, adding a Buyer is as simple as creating a Buyer delegate class like so:

class Buyer < DelegateClass(Person)
  delegate :id, :to => :__getobj__

  def items
    Item.for_buyer_id(id)
  end

  def is_buyer?
    purchased_items.present?
  end
end

Now when you need to make Person driven by something other than ActiveRecord::Base, your delegate classes don’t change at all.

Delegate classes aren’t the solution to every problem, and certain behavior, such as #reload can be very confusing at first:

person = Person.find(1)
seller = Seller.new(person)
seller.class # => Seller
seller.reload.class # => Person

Another gotcha is that id doesn’t delegate by default, so you have to add the following line to make sure you get the ActiveRecord id:

  delegate :id, :to => :__getobj__

However, delegate classes can go a long way to making your code more supple.

Comments
  1. Matthew O'Connor says:

    In the Buyer example you mean “class Buyer < DelegateClass(Person)”, you typo’ed and wrote Person by mistake.

  2. Jeff Dean says:

    Thanks. Fixed.

  3. grosser says:

    another solution can be [concern](http://github.com/grosser/concern)
    it looks like this:

    class Person
    concern ‘person/selling’
    end

    class Person::Selling < Concern def looking_good? sold_items > 5 # sold items is a column on person
    end
    end

    Person.first.selling.looking_good?

  4. iain says:

    I usually just include modules actually. It’s a bit more low tech, but has it’s problems.
    When a functionality is spread around on multiple models, I might even delegate something:

    class Person
      include Search
    end
    class Post
      include Search
    end
    module Search
      def self.included(base)
        send(base.name.underscore) if respond_to?(base.name.underscore)
      end
      def self.post
        # do something specific for Post
      end
      def self.person
        # do something specific for Person
      end
    end
    

    I use this for instance to define search indexing on a central place.

    When using modules, you do have to watch out for name clashes. And always argue which approach is best suited for your problem.

  5. drboolean says:

    Thanks for this! I can’t wait to try it.

    Is there a reason you prefer this over extending Forwardable?

    An ironic twist is that the open/closed principal will surely be violated with every change to the person class as you have to append the method to the delegate list (same with forwardable).

    I typically end up adding a method_missing for decorators or proxies – I wonder if this has a niche use as well.

  6. Mario says:

    In your example, why not just use STI? It seems like it would be a lot more flexible. If you already had data in the persons table, it would be relatively easy to add a migration to create a type field and to set some default value. You could then use ActiveRecord to manage all the other relationships for you. You would never need write “is_author?” methods.

    STI seems like it would be easier to manage from a performance perspective as well. Let’s say I needed a list of all authors or sellers. Using delegate class, I would have to check another model every time and you wouldn’t be able to index for user types. With STI, I could just check the persons table and would be able to set up a composite index and ID and TYPE.

    Using STI, there would be no need for modules, as methods could be inherited from the parent class.

    The only advantage that I see is that it might limit the amount of refactoring that you would need to do if you had an existing application with a larger code base.

    I’m eager to learn the best way to solve these sorts of problems, but am wondering if there are other benefits I’m missing.

  7. Mario says:

    P.S. The default profile photo.png seems to be missing.

    P.S.S. can you include an anchor for the comment form and include that in your failure redirect? I kept missing the captcha and getting redirected to the very top of the page.

    I hope that’s helpful. Thanks!

    -Mario

  8. Joseph Palermo says:

    @iain:
    Modules separate the code into separate files, but all of your objects still end up with a superset of the methods. This seems bad from a OO perspective. You could include the modules into instances instead of into the base class, but doing runtime includes can be a big performance hit since it globally invalidates the method lookup cache (in MRI at least).

    @Mario:
    In the example above he Jeff said you can’t use inheritance because a Person can be both a Buyer and a Seller. If you use inheritance they have to be one or the other.

  9. Jeff Dean says:

    @grosser – Concerns has an awesome dsl, thanks for the link. I think that’s a better solution than including modules, but it still requires you to reopen the original class. If you wanted to do something like move the Seller functionality into a Rails Engine, or a gem, you’d have a dependency inversion issue, where your gem or plugin depends on code in a specific app, or you’d have to re-open the Person class in your app and specify these concerns.

    @drboolean – DelegateClass automatically delegates all public instance methods (except for Kernel’s public instance methods) to the class specified, and also delegates any missing methods to the instance passed in via method_missing. So you shouldn’t have to open up your delegate class ever.

    That said, using Forwardable and explicitly naming the methods that get delegated would make the code more self-documenting, and force you to adhere to a more strict interface.

  10. Maksim Horbul says:

    I would rather use STI in the case described in the article. But that was cool to learn about delegation in Ruby :) Unfortunately I did not understand how the relationships will work in the example from the article. You have the empty Person class which extends the AR. Where the has_many, belongs_to and validates_* should go in that case ?

  11. Jeff Dean says:

    STI is not an option here. With STI, a Person could only be a Seller _or_ a Buyer. The requirement described here involves a Person being both a Buyer _and_ a Seller.

  12. Jeff Dean says:

    Rails makes it very hard to use validations without modifying the Person class, so in the scenario I would likely keep the validations in Person. Rails 3 will make this a little easier with the validates_with method.

    Associations are much easier – in the examples above I showed how you could replace the has_many relationships. You could use a similar technique for has_one.

    belongs_to is trickier, since you have to store some foreign key on the Person table. In these cases, I normally try to create a new table that has the foreign_key to the Person, and whatever other table is being referenced. Then I can use a has_one, which is easy to capture in the delegate class. This makes it easier to move related functionality to a gem or engine if necessary.

  13. Maksim Horbul says:

    I have re-read the article two more times and got it now. Appreciate your time. Thank you.

  14. drboolean says:

    Gotcha! I have a new favorite tool.

  15. […] essa classe e não tenho certeza que é a melhor padrão por esse problema, mas gosto dela. Tem um artigo por Jeff Dean de Pivotal Labs que explica a ideia de DelegateClass muito […]

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *