Refactor ruby on rails

Tại sao cần refactor code

Refactoring là hành động thay đổi mã của ứng dụng nhưng không làm thay đổi hành vi thay vì nâng cao chất lượng của ứng dụng. Việc này được dùng để cải thiện khả năng đọc, giảm độ phức tạp, tăng khả năng bảo trì và mở rộng trong tương lai của hệ thống.

Nói nôm na là viết code cho dễ hiểu, chạy nhanh, tránh để bị ăn chửi khi thằng khác đọc code của mình.!

Hoặc đơn giản hơn là tìm được đoạn code cần sửa 1 cách dễ dàng khi maintain hay fix bug.

Trong bài viết buộc phải sử dụng dấu () bao ngoài dấu (@) vì viblo nhận diện (@) là tag.

Refactor view

1. Render partial, render collection

  • Để đảm bảo tính DRY (Don't repeat yourself) những đoạn code dùng chung nên đưa vào render partial
<h1>Listing Books</h1>
<table>
  <tr>
    <th>Title</th>
    <th>Summary</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>
<% (@books).each do |book| %>
  <tr>
    <td><%= book.title %></td>
    <td><%= book.content %></td>
    <td><%= link_to "Show", book %></td>
    <td><%= link_to "Edit", edit_book_path(book) %></td>
    <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
  </tr>
<% end %>
</table>
<br>
<%= link_to "New book", new_book_path %>

refactor:

<h1>Listing Books</h1>
<table>
  <tr>
    <th>Title</th>
    <th>Summary</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>
  <%= render partial: "book", collection: (@books), as: :book %>
</table>
<br>
<%= link_to "New book", new_book_path %>

_book.html.erb

  <tr>
    <td><%= book.title %></td>
    <td><%= book.content %></td>
    <td><%= link_to "Show", book %></td>
    <td><%= link_to "Edit", edit_book_path(book) %></td>
    <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
  </tr>

2. Code trong view nên đơn giải hết mức có thể, vì code html đã đủ rối mắt lắm rồi 😦(

<% if user.role == "member" %>
  <%= render "member_nav" %>
<% else %>
  <%= render 'guest_nav' %>
<% end %>

  <%= render "#{user.member? 'member' : 'guest'}_nav" %>

3. NULL Object - delegate

<%= question.user.name  %>

Trong view sử dụng 2 dấu chấm theo mình là điều tối kỵ. (Law_of_Demeter) Vì nếu question kia không có user thì sẽ bị lỗi

No method name for nil class

Trong khi đúng ra nếu nil thì không in ra và chạy tiếp. Giải pháp ở đây là sử dụng delegate

Class Question  < ActiveRecord::Base
  delegate :name, to: :user, prefix: :true, allow_nil: true
end

<%= question.user_name  %>

4. View Object

Như đã nói ở trên, sử dụng 2 dấu trong views là không ổn. Cũng không nên sử dụng helper vì các method ở trong helper có thể gọi ở bất kỳ controller nào, sẽ gây nhầm lần hoặc phình to Application controller.

Vậy nên cần đưa các logic đó vào 1 object (Tất nhiên ko phải model vì sẽ làm Fat Model)

Bạn có thể sử dụng các gem dưới đây

https://github.com/drapergem/draper

http://nithinbekal.com/posts/rails-presenters/

Hoặc có thể tự định nghĩa như sau:

class Support::QuestionSupport
  attr_reader :question

  def initial question
    (@question) = question
  end

  def related_question
    (@related_question) ||= Question.related_question.limit(10).order(created_at: :desc)
  end
end

#Khởi tạo trong controller
(@support) = Support::QuestionSupport.new(@question)
#gọi ở view:
 <%= render partial: "question", collection: (@support.related_question), as: :question %>

Viết như vậy vừa có thể test rspec được, vừa có thể caching related_question nếu related_question được gọi nhiều lần.

5. Form Objects

Nếu view object là để hiển thị thì form object là để giúp ta tạo ra form dùng để update nhiều model trong 1 request. Thay vì sử dụng accepts_nested_attributes_for đôi khi ta cần sử dụng form object để simplifying multi-model forms Ý tưởng ở đây là tạo ra 1 Object tương tự model nhưng khi save thì save vào 2 table

class CreateSurvey
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attribute :title, String
  attribute :questions, Array[String]

  validates :title, presence: true

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

  private

  def persist!
    transaction do
      (@survey) = Survey.create!(title: title)
      (@questions) = questions.map{|question_text| Question.create(text: question_text)
    end
  end
end
SurveysController < ApplicationController
  def create
    (@survey) = CreateSurvey.new(params[:survey])

    if (@survey).save
      # logic if successful
    else
      # logic if unsuccessful
    end
  end
end

https://github.com/apotonick/reform

http://crypt.codemancers.com/posts/2013-12-18-form-objects-validations/

http://hawkins.io/2014/01/form_objects_with_virtus/

Tổng kết

Như vậy chúng ta đã có 1 vài phương pháp refactor views:

  • Sử dụng partial để tránh lặp code
  • Sử dụng yield, content_for, localassigns để render view 1 cách linh hoạt
  • Sử dụng view_objectdelegate để tránh xử lý logic ở view, tránh fat model
  • Sử dụng form object để update nhiều model trong 1 request.

Hẹn gặp lại các bạn trong các bài viết tiếp theo.

Tài liệu tham khảo

https://viblo.asia/dieunb/posts/AQ3vVeLnGbOr

https://allenan.com/refactoring-rails-views/