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