Misunderstanding the Law of Demeter

http://www.dan-manges.com/blog/37

 

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
    # an associated object
    # could be a belongs_to in Rails
  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

  # attribute delegation
  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
  # behavior delegation
  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.

你可能感兴趣的:(sun)