Friday 21 September 2012

Follow the Law of Demeter

Solution : Follow the Law of Demeter

You have some models, and you have view code that looks like the following:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices
end

class Invoice < ActiveRecord::Base
  belongs_to :customer
end

This code shows a simple invoice structure, with a customer who has a single address. The view code to display the address lines for the invoice would be as follows:

<%= @invoice.customer.name %>
<%= @invoice.customer.address.street %>
<%= @invoice.customer.address.city %>
<%= @invoice.customer.address.state %>
<%= @invoice.customer.address.zip_code %>

Ruby on Rails allows you to easily navigate between the relationships of objects and therefore makes it easy to dive deep within and across related objects. While this is really powerful, there are a few reasons it's not ideal. For proper encapsulation, the invoice should not reach across the customer object to the street attribute of the address object. Because if, for example, in the future your application were to change so that a customer has both a billing address and a shipping address, every place in your code that reached across these objects to retrieve the street would break and would need to change.

To avoid the problem just described, it's important to follow the Law of Demeter, also known as the Principle of Least Knowledge. 

In Rails, this could be summed up as "use only one dot." for example, @invoice.customer.name breaks  the Law of Demeter, but @invoice.customer_name does not. Of course, this is an over simplification of the principle, but it can be used as a guideline.

To follow the Law of Demeter, you could rewrite the code above as follows:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices

  def street
    address.street
  end

  def city
    address.city
  end

  def state
    address.state
  end

  def zip_code
    address.zip_code
  end

end

class Invoice < ActiveRecord::Base
  belongs_to :customer

  def customer_name
    customer.name
  end
  
  def customer_street
    customer.street
  end

  def customer_city
    customer.city
  end

  def customer_state
    customer.state
  end

  def customer_zip_code
    customer.zip_code
  end 

end

And you could change the view code to the following:

<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>

In this new code, you have abstracted out the individual methods that were originally being reached by crossing two objects into individual wrapper methods on each of the models.

The downside to this approach is that the classes have been littered with many small wrapper methods. If things were to change, now all of these wrapper methods would need to be maintained. And while this will likely be considerably less work than changing hundreds of references to invoice.customer.address.street  throughout your code, it's still an annoyance that would be nice to avoid.

Fortunately, Ruby on Rails includes a function that addresses the first concern. this method is the class-level delegate method. This method provides a shortcut for indicating that one or more methods that will be created on your object are actually provided by the related object.

Using this delegate method, you can rewrite your example like this:

class Address < ActiveRecord::Base
  belongs_to :customer
end

class Customer < ActiveRecord::Base
  has_one :address
  has_many :invoices

  delegate :street, :city, :state, :zip_code, :to => :address
end

class Invoice < ActiveRecord::Base
  belongs_to :customer

  delegate :name, :street, :city, :state, :zip_code, :to => :customer, :prefix => true

end

In this situation, you don't have to change your view code; the methods are exposed just as they were before:

<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>



No comments:

Post a Comment