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