5 ways to refactor Rails views
Mở đầu
Khi bạn phát triển một ứng dụng web, việc code nở ra rất nhiều theo thời gian là điều khó tránh khỏi. Thêm vào đó, khi mở rộng code, spec thay đổi hoặc trong giai đoạn fix bug, bạn sẽ gặp rất nhiều khó khăn nếu không quản lý tốt những dòng code của mình từ đầu. Refactor rails views code cũng là một kĩ năng quan trọng và cần thiết cho bạn. Trong bài viết này, mình sẽ đưa ra một vài cách để bạn có thể refactor lại views code của mình.
Cách để refactor views code
1. Partials
Đây là một phương pháp cơ bản và phổ biến nhất mà chúng ta có thể nghĩ tới. Partials được xây dựng trong Rails để có thể tái sử dụng những đoạn code view hoặc chia nhỏ từng phần của file view tổng thể, khiến cho chúng ta dễ theo dõi được cấu trúc của 1 file view. Một partial thông thường sẽ được gọi thế này
# app/views/employees/new.html.erb
<h1>New Employee</h1>
<%= render 'form' %>
<%= link_to 'Back', employees_path %>
Nó sẽ render ra file _form.html.erb
trong cùng thư mục với file new.html.erb
. Trong trang index employee, bạn có thể render ra 1 list các @employees bằng cách
<%= render @employees %>
Rails sẽ tìm kiếm partial tên _employee
và sử dụng nó để render mỗi employee trong list @employees collection.
Ngoài ra nhiều lúc, bạn cũng sẽ cần đến render partial: "shared/employee', collection: @employees
để render ra một file partial nằm hoàn toàn ở 1 thư mục khác :D
Resources
- Layouts and Rendering in Rails (Official Docs)
- Action View Partials (Official Docs)
- Partials in Ruby on Rails
2. Decorators
Decorator pattern được sử dụng để đóng gói các logic hiển thị mà không thuộc về model, nhưng cũng không thuộc về views :D Ví dụ như bạn có một blogging application, hiển thị ngày public của một article theo một format (vd Feb 28th, 2017) hoặc là “Draft” nếu như nó chưa được publish. Khi đó, đoạn code của bạn sẽ là:
<article>
<span class="publication-status">
<% if @article.published? %>
Published at: <%= @article.published_at.strfitme("%B #{@article.published_at.day.ordinalize}, %Y")
<% else %>
Draft
<% end %>
</span>
...
</article>
Nếu bạn để ý, khi có những logic hiển thị nằm trong view file, chúng ta sẽ rất khó để nhìn ra cấu trúc tổng thể của toàn bộ file view.
Bạn có thể nghĩ đến việc dùng View Helpers của Rails như một giải pháp thay thế việc đặt logic ở trong view. Tuy nhiên, sử dụng Helper có một vài hạn chế:
* Helpers được include trong tất cả các views, tức là bạn sẽ phải cẩn trọng trong việc đặt tên hàm vì nó sẽ rất dễ bị conflict. Thêm vào đó, 1 view có thể gọi những method mà không có tác dụng gì cho nó thì cũng không hợp lý. :D
* Helpers là các modules, do đó bạn sẽ không thể truy cập từ 1 object. Điều đó có nghĩa là bạn sẽ phải truyền 1 object vào phương thức giống như thế này: article_published_at_date(article)
Draper là một gem rất phổ biến cung cấp cho banj định nghĩa các decorator pattern để mở rộng một Active Record object với logic hiển thị mà không cần đặt hết chúng vào trong model.
class ArticleDecorator < Draper::Decorator
delegates_all
def publication_status
if is_published?
"Published at: #{published_at}"
else
"Draft"
end
end
def published_at
object.published_at.strfitme("%B #{published_day}, %Y")
end
private
def published_day
object.published_at.day.ordinalize
end
end
Để object của chúng ta gọi được decorator, ta chỉ việc thêm nó vào trong controller như sau:
class ArticlesController < ApplicationController
def show
@article.find(params[:id]).decorate
end
end
Khi đó, trong view file sẽ chỉ còn lại là:
<article>
<span class="publication-status">
<%= @article.publication_status %>
</span>
...
</article>
Sẽ không còn những logic không cần thiết trong view. Chúng ta có thể dễ dàng nhìn, đọc và hiểu được điều gì đang xảy ra trong view và cấu trúc tổng thể của file view là như thế nào :D
Resources
3. Null Objects
Sử dụng Null Object pattern để tranh việc gặp phải các logic phân nhánh có điều kiện. Giả sử chúng ta có một ứng dụng mà người dùng có thể là admins, customers hoặc guest nếu họ không đang nhập. Tùy thuộc vào role của từng loại user, chúng ta sẽ phải show ra từng loại header navigation khác nhau.
<% if user_signed_in? %>
<% if current_user.role == 'admin' %>
<%= render 'admin_nav' %>
<% elsif current_user.role == 'customer' %>
<%= render 'customer_nav' %>
<% end %>
<% else %>
<%= render 'guest_nav' %>
<% end %>
Chúng ta đang sử dụng partials, trông code có vẻ khá tốt. Tuy nhiên, đoạn code ở đây chưa được linh hoạt cho lắm. Mỗi khi có 1 role mới được thêm vào, chúng ta lại sẽ phải tìm những chỗ nào tương ứng vs từng loại user mà sửa lại code, thêm 1 đoạn elsif nữa vào. Nếu phải làm như thế, chúng ta sẽ phải sửa ở rất nhiều nơi, mà k có gì đảm bảo là ta đã sửa hết cả :v Sử dụng một chút refactoring, chúng ta có thể linh động trong việc render partial dựa trên user’s role như sau:
<% if user_signed_in? %>
<%= render "#{current_user.role}_nav" %>
<% else %>
<%= render 'guest_nav' %>
<% end %>
Great! Tuy nhiên chúng ta vẫn bỏ sót trường hợp null, đó là khi current_user bị nil. Khi đó current_user sẽ trả về nil -> Không render ra được partial tương ứng. Đó là lúc Null Object tỏ ra hữu ích. Null Object là một pattern rất đơn giản, nhưng nó mang tới rất nhiều ứng dụng. Một Null Object là một Plain old Ruby object mà chứa những method giống như non-null object, chỉ khác ở giá trị trả về. Kết hợp với Ruby’s duck typing, chúng ta sẽ có thể coi Null Object giống như những normal objects đang được gọi.
class NullUser
def role
'guest'
end
end
Bây giờ, trong ApplicationCOntroller, nơi current_user được định nghĩa, chúng ta sẽ trả về NullUser thay cho việc tra về nil nếu người dùng chưa đăng nhập.
class ApplicationController
def current_user
super || NullUser.new
end
end
Điều đó có nghĩa là trong views code của chúng ta giờ chỉ còn lại 1 đoạn rất ngắn gọn:
<%= render "#{current_user.role}_nav" %>
Resources
4. Form Objects
Form objects là một công cụ hữu ích khi bạn có một multi-modle forms hoặc các form logic phức tạp. Thay vì việc sử dụng accepts_nested_attributes_for
và handing với việc validations ở nhiều model khác nhau, form object cho phép bạn nhóm các thuộc tính của form và validation vào 1 object duy nhất.
Ví dụ bạn có một model Survey
. Nếu bạn muốn tạo một survey, bạn cần tạo rất nhiều Question
, bạn nên xem xét có nên sử dụng Form Object để tập trung xử lý logic của việc tạo Survey vào 1 chỗ không. ( Đôi khi phần xử lý logic này diễn ra trong controller, nên sử dụng Form object cũng là 1 cách hữu ích để banj có thể làm thin controller của mình :D )
Để dễ hiểu hơn, mình sẽ sử dụng gem Virtus để tạo ra 1 object mới (trông nó sẽ khá giống với 1 ActiveRecord model ) nhưng bên trong nó sẽ bao gồm các options cần thiết để tạo ra Form Objects
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
Trong controller, chúng ta sẽ tạo 1 form object với params được gửi lên từ form trong view, sử dụng CreateSurvey object ở trên.
class SurveysController < ApplicationController
def create
@survey = CreateSurvey.new(params[:survey])
if @survey.save
# logic if successful
else
# logic if unsuccessful
end
end
end
Resources
- Reform Gem
- Form Object Validations in Rails 4
- Form objects with Virtus
- Form object (Rails Cast)
- 7 Patterns to refactor Fat ActiveRecord Models
5. Alternate Templating Languages
Erb là một template language tốt, đủ dùng và rất phổ biến. Tuy nhiên, một vài ngôn ngữ nâng cao khác như Haml và Slim sẽ giúp cho bạn tối giản hết mức những đoạn code HTML của mình, khiến cho những dòng code trở nên ngắn ngọn hơn rất nhiều. ERB
<section class=”container”>
<h1><%= post.title %></h1>
<h2><%= post.subtitle %></h2>
<div class=”content”>
<%= post.content %>
</div>
</section>
Haml
%section.container
%h1= post.title
%h2= post.subtitle
.content
= post.content
Slim
section.container
h1= post.title
h2= post.subtitle
.content= post.content
Chọn một template language thích hợp là một việc mang tính cá nhân. Nếu bạn cảm tháy thoải mái với HTML tags, ERB sẽ là một lựa chọn tốt và hỗ trợ rất nhiều cho bạn. Nếu bạn muốn ngắn gọn, nhìn code sạch, dễ nhìn hơn, hãy thử sử dụng Haml hoặc Slim và trải nghiệm :D
Resource
- Haml
-
Kết luận
Nếu bạn đọc tới đây thì chắc bạn cũng nhận ra rằng: > Refactor code view KHÔNG làm cho code tổng thể của project giảm đi, nó chỉ bê code từ chỗ này cho sang chỗ khác =]]
Tuy nhiên, mọi thứ sẽ trở về đúng vị trí của nó, khiến bạn sẽ dễ dàng hơn rất nhiều trong việc mở rộng và quản lý code view của mình. Hy vọng bài viết này có ích cho bạn! :)