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.
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
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 AuthorMore Content by Jeff Dean