The Law of Demeter is not easy to understand when reading it for the first time. Quoting the
definition from Wikipedia:
More formally, the Law of Demeter for
functions requires that a method M of
an object O may only invoke the methods
of the following kinds of objects:
1. O itself
2. M's parameters
3. any objects created/instantiated within M
4. O's direct component objects
In particular, an object should avoid invoking
methods of a member object returned by another method.
The Wikipedia article summarizes it as "Only talk to your immediate friends." Developers seems to recognize violations by looking for more than one "dot", such as:
foo.bar.baz
Here, "foo" is talking to "baz" through "bar". The solution (in Ruby) is to use Forwardable and set up a delegation. Here is an example:
class Foo
extend Forwardable
def_delegator :bar, :baz
def bar
end
end
With this delegation in place, we can reduce the two "dots" into one by doing: foo.baz. Forwardable and def_delegator take care of going from "foo" through "bar" to "baz." While this may seem to solve our problem, the solution has a significant misconception.
Delegation is an effective technique to avoid Law of Demeter violations, but only for behavior, not for attributes.
To explain, I need a better example than foo/bar/baz. One of the classic examples I have seen to explain this is a Paper Boy, a Customer, and a Wallet. A paper boy needs to collect money from his customers. A customer stores his/her money in a wallet. Here is how this might be modeled violating the Law of Demeter:
class Wallet
attr_accessor :cash
end
class Customer
has_one :wallet
end
class Paperboy
def collect_money(customer, due_amount)
if customer.wallet.cash < due_ammount
raise InsufficientFundsError
else
customer.wallet.cash -= due_amount
@collected_amount += due_amount
end
end
end
Hopefully apparent from this example is that a paperboy should not be taking cash out of a customer's wallet. This is a clear Law of Demeter violation. Going back to the simple way to recognize this mistake, we can see two dots in "customer.wallet.cash". Here is how this might change with attribute delegation.
class Wallet
attr_accessor :cash
end
class Customer
has_one :wallet
def cash
@wallet.cash
end
end
class Paperboy
def collect_money(customer, due_amount)
if customer.cash < due_ammount
raise InsufficientFundsError
else
customer.cash -= due_amount
@collected_amount += due_amount
end
end
end
This example is only slightly different. A customer now has cash, which simply delegates to cash in the wallet. Now in the Paperboy collect_money method, we don't have two dots, we just have one in "customer.cash". Has this delegation solved our problem? Not at all. If we look at the behavior, a paperboy is still reaching directly into a customer's wallet to get cash out. That's not good. However, if instead of delegating attributes, we delegate behavior, we will end up with a much better OO design.
class Wallet
attr_accessor :cash
def withdraw(amount)
raise InsufficientFundsError if amount > cash
cash -= amount
amount
end
end
class Customer
has_one :wallet
def pay(amount)
@wallet.withdraw(amount)
end
end
class Paperboy
def collect_money(customer, due_amount)
@collected_amount += customer.pay(due_amount)
end
end
Again, the change is simple, but our OO-ness and class responsibilities are much better. Notice the way delegation is done now. The pay method on customer simply delegates to the withdraw method on the wallet. Even with the argument on the pay method, this could also be implemented by using Forwardable.
Is the Customer#pay method, with doing nothing but a simple delegation, valuable? Yes. We want our paper boy to know as little as possible about the customer. It's okay for the paper boy to know the customer has a method available to pay him. The paper boy knowing the customer has a wallet (and even further, that the wallet has cash), is not okay. This is what the Law of Demeter is about.
Thinking again about attribute/getter/setter delegation, it gives classes too much knowledge about other classes. This includes classes that are far away from each other in the domain model. Going back to the first example, we don't want a paper boy to know a customer has a wallet. Onto the second example, we really don't want a customer to know a wallet has cash if we don't need to. The behavior delegation is a much better way to solve this problem.
Because I'm not confident this example is perfectly clear (after all, why wouldn't a customer know he has cash, why should that be hidden (read: encapsulated) in the wallet?), here is another example, and what I think has caused some overuse of attribute delegations:
An Order belongs_to a Customer. Let's say we're developing a Rails view to display order information, and this should include details on the customer. We might write the view like this:
<%= @order.customer.name %>
Two dots! Demeter violation? No. If you thought it was, you might have a delegation in your model, something like:
class Order
extend Forwardable
def_delegator :customer, :name, :customer_name
end
Our view now becomes:
<%= @order.customer_name %>
We've traded a dot for an underscore. And thinking about this further, why should an order have a customer_name? We're working with objects, an order should have a customer who has a name. Adding these attribute delegations also decreases maintainability.
The crux of this is that webpage views aren't domain objects and can't adhere to the Law of Demeter. Clearly from the examples of behavior delegation the Law of Demeter leads to cleaner code. However, when rendering a view, it's natural and expected that the view needs to branch out into the domain model. Also, anytime something in a view dictates code in models, take caution. Models should define business logic and be able to stand alone from views. If this "train-wreck" method calling in your views is bothersome, there is a better solution that I will blog about later.
Focus on delegating behavior more than attributes. This ties well into the
"Tell, don't ask." principle. With the paper boy example, our code is much better when a customer tells his/her wallet to withdraw an amount, rather than asking the wallet for how much cash it has and then doing the logic in the customer model.