Tuesday, May 23, 2006

Law of Demeter and Forwardable

While developing software I often see Law of Demeter violations. These days I seem to see more of this, probably because Ruby on Rails allows you to easily navigate between objects based on table relationships. For example, if you are working on a partial that displays a charge from a credit card statement it wouldn't be surprising to see:
charge.statement.customer.name
The above code could be used to display the customer's name in the partial. Unfortunately, it assumes statement will have a customer and customer will have a name. This is easily fixed by changing charge to only talk to it's friends.
charge.customer_name
The simple fix to support this is to define customer_name in charge, and customer_name in statement
class Charge
def customer_name
statement.customer_name
end
end

class Statement
def customer_name
customer.name
end
end
This change is simple enough; however, as the list of methods that require delegation grows your class can become littered with delegation code.

Luckily, Forwardable is included in the standard library. Forwardable allows you delegate method calls to an object, on a method by method basis. Using Forwardable the above code becomes:
class Charge
extend Forwardable
def_delegators :statement, :customer_name
end

class Statement
extend Forwardable
def_delegator :customer, :name, :customer_name
end
Forwardable becomes even more valuable when you need to delegate several methods
def_delegators :amount_info, :units, :fractions, :currency
For more info on Law of Demeter and it's advantages check out the definition on Wikipedia. For more info on Forwardable check out the documentation on Ruby-Doc.

5 comments:

  1. Anonymous3:36 AM

    In Rails, you can use the delegate method. There's no documentation for it in the current API, but you can view the doc changeset.

    Looking at the rdoc for Forwardable, I can say that I don't like it. There's no reason to make your class inherit from it (and that wouldn't jive with Rails models, since they all have to inherit from AR). A simple class-level method, like delegate, ought to suffice. I understand that you're not using/discussing Ruby solely within the Rails context. In this case, the Rails solution is superior because it's more flexible, because it allows you to compose objects without restricting the class heirarchy.

    The advantage that Forwardable has is that you can use it to change the interface - delegate simply takes a request and passes it along to an association. Honestly I don't see how Forwardable is very helpful...it doesn't look like it saves you from typing much, and you won't have a clear picture of the interface using RDoc.

    Make Forwardable a module, simplify its class methods a bit:
    delegate :foo, :bar, :to => :object
    delegate :foo, :to => :object, :as => :superfoo

    and it'd be a lot more useful in my opinion.

    ReplyDelete
  2. Anonymous11:09 AM

    I think you've misunderstood how extend works. Since this seems to be pretty common I'll probably do an entry on that topic tonight.

    stay tuned.

    ReplyDelete
  3. Anonymous1:20 PM

    This kind of automatic delegation doesn't really fix the violation of the Law of Demeter. In fact neither does the manual delegation that you are trying to avoid.

    The train-wreck coding that delegation fixes is a symptom of a problem rather than the problem itself. The real problem is that the object model is such that 'distant' objects are talking to each other. The model needs to be refactored so that objects only need to talk to 'near-by' objects (their 'friends').

    I'd like to show what I mean based on your example but I don't understand how the display code would need to get the customer name from the charge. Can you give a bit of context (or a different example)?

    Of course the real problem may be that there is no object model. I think this is a huge problem with all these fancy O/R mapping technologies like ActiveRecord and SQLObject. We have a data model for the DB but that gets mapped straight onto the objects so we don't ever stand back and think about the object model. This results in code that violates a lot more than just the Law of Demeter.

    Honestly, I'd leave the train wrecks in. At least that way we can see that there are problems.

    ReplyDelete
  4. Anonymous1:36 PM

    Ben,
    I agree. Unfortunately, Rails apps in general depend heavily on the AR models.

    The example I used is fictional. However, the general idea was that a page would display all the charges from a statement. Each charge will be displayed in a partial and will need to display the customer name.

    Though, I do think there is value in delegating over the train wrecks. When a change needs to happen, like customer name becoming customer full_name, you only have to change the delegation definition in Statement instead of changing all the train wrecks.

    ReplyDelete
  5. A DRY way to apply Law of Demeter with demeter gem.

    http://github.com/emerleite/demeter
    http://gemcutter.org/gems/demeter

    ReplyDelete

Note: Only a member of this blog may post a comment.