Rails : Refactoring principles

the Law of Demeter:

Now 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.

This is also called

Principle of Least Knowledge.

object can call methods on a related object but that it should not reach through that object to call a method on a third related object. In Rails, this could be summed up as

“use only one dot.”

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 %>

The downside to this approach is that the classes have been littered with many small wrapper methods

Fortunately, Ruby on Rails includes a function that addresses the first concern. This method is the class-level delegate method

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 %>
class Greeter < ActiveRecord::Base
  def hello
    'hello'
  end

  def goodbye
    'goodbye'
  end
end

class Foo < ActiveRecord::Base
  belongs_to :greeter
  delegate :hello, to: :greeter
end

Foo.new.hello   # => "hello"
Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s