DelegateClass rocks my world

January 21, 2010 Jeff Dean

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.

About the Author

Biography

Previous
tgethr email collaboration and Pivotal Tracker
tgethr email collaboration and Pivotal Tracker

If you've been looking for a way to turn emails into Tracker stories, take a look at tgethr. It's an email ...

Next
NYC Tech Talk: 19 Jan 2010
NYC Tech Talk: 19 Jan 2010

Pivotal NYC was lucky enough to have Ben Stein in the office to give a beta presentation entitled "Beyond t...