Let's take a look at this piece of code:
<% @posts.each do |post| %>
<%= link_to post.user.email.split('@').first, user_path(post.user_id) %>
<% post.comments.each do |comment| %>
<%= link_to comment.user.email.split('@').first, user_path(comment.user_id) %> <%= comment.text %>
<% end %>
...
<% end %>
In this code above, let's specifically look at the following pieces of code:
post.user.email.split('@').first
...
comment.user.email.split('@').first
Notice how the code above all call attributes of another object.
For example, in post.user.email
, email
is an attribute of user
, which is an attribute of post
.
This violates the Law of Demeter.
The Law of Demeter
In the simplest terms, the Law of Demeter states that an object should not call methods through another object. It is also called the principle of least knowledge.
The reason and motive for this guideline is to keep the code maintainable and adaptable. By keeping an object from calling methods of another object, we keep the objects less dependent on other objects. This is related to the concept of loose coupling.
Loose coupling refers to code in which objects do not depend on each other and has little or no knowledge about other objects. Tightly coupled code can lead to unmaintainable code - if you change one piece of the code, it might lead another piece of the code base to break.
How can we abide by the law of demeter in Rails?
What we can do is create methods within the class that return the specific method of the other object we are trying to access. For example, in our Post.rb
, we can write the following methods:
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments, dependent: :destroy
has_one_attached :photo
def user_email
user.email
end
end
Now, instead of accessing the email
of the user
directly (post.user.email.split('@').first
), we have created a method inside of the Post
class that we can use to access the same data: post.user_email
.
However, this can become repetitive, and fortunately Rails provides a convenient method to achieve this; the delegate
method.
We can refactor the code like this:
In Post.rb
:
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments, dependent: :destroy
has_one_attached :photo
delegate :email, to: :user, prefix: true
end
In Comment.rb
:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
delegate :email, to: :user, prefix: true
end
In index.html.erb
:
<!-- link_to post.user.email.split('@').first, user_path(post.user_id) -->
<%= link_to post.user_email.split('@').first, user_path(post.user_id) %>
<!-- link_to comment.user.email.split('@').first user_path(comment.user_id) -->
<%= link_to comment.user_email.split('@').first, user_path(comment.user_id) %>
Setting
prefix: true
gives you a method like this:post.user_email
. Settingprefix: false
would result in a method without the prefix -post.email
.
To make the code look even shorter, create a method in helper file like below:
def user_name(email)
email.split('@').first
end
And replace the erb file as below:
<!-- link_to post.user_email.split('@').first, user_path(post.user_id) -->
<%= link_to username(post.user_email), user_path(post.user_id) %>
<!-- link_to comment.user_email.split('@').first user_path(comment.user_id) -->
<%= link_to username(comment.user_email), user_path(comment.user_id) %>
By using delegate
, we can follow the Law of Demeter, which will lead to a more maintanable codebase.