Refactor Ruby on Rails Code
Bài đăng này đã không được cập nhật trong 6 năm
Refactor Ruby on Rails Code
Đôi khi, chúng ta không thích một yêu cầu chức năng bởi vì cách dễ nhất để giải quyết vấn đề đó là viết bad code (mã xấu) và chúng ta không nghĩ ra được giải pháp nào khác trong đầu. Điều này có thể khiến các developer tìm thấy rất ít kết quả thông qua các trang Ruby Toolbox, github, developer blog, và StackOverflow tìm kiếm một gem hoặc plugin hoặc code mẫu mà khiến chúng ta cảm thấy tốt hơn về chính mình.
Bạn hoàn toàn có thể viết bad code, đôi khi code Rails xấu lại dễ dàng để refactor thành code đẹp hơn là một giải pháp tồi đã cài đặt trong thời gian gấp rút. Dưới đây là quy trình refactor Rails code tôi thích làm theo khi chỉnh sửa các vấn đề đối với các giải pháp tạm thời kinh khủng của mình.
Views
Step 1. Bắt đầu với Views
Giả sử chúng ta đang bắt đầu cài đặt 1 ticket cho 1 feature mới và khách hàng nói với chúng ta rằng: "Những vị khách truy cập hệ thống nên có thể xem được danh sách các project đang hoạt động trên trang chào mừng“.
Ticket này yêu cầu một sự thay đổi có thể nhìn thấy được, vì vậy, nơi hợp lý để bắt đầu làm việc sẽ là trong phần Views. Vấn đề này rất đơn giản và là một vấn đề mà chúng ta đã được đào tạo để giải quyết nhiều lần. Tôi sẽ giải quyết nó theo cách được gọi là The Wrong Way
và chứng minh làm thế nào để refactor giải pháp của tôi đối với các phần thích hợp. Giải quyết vấn đề The Wrong Way có thể giúp chúng ta vượt qua được cái bẫy của việc không biết giải pháp đúng đắn.
Để bắt đầu, giả sử chúng ta có một model được gọi là Project
với một thuộc tính boolean là active
. Chúng ta muốn có được một danh sách tất cả các project mà có active
bằng true
, vì vậy chúng ta có thể sử dụng Project.where (active: true)
, và lặp lại nó với mỗi block.
app/views/pages/welcome.slim:
ul.projects
- Project.where(active: true).each do |project|
li.project = link_to project_path(project), project.name
Tôi biết bạn đang nói gì: "Điều đó sẽ không bao giờ pass một cuộc review code" hoặc "Khách hàng của tôi chắc chắn sẽ sa thải tôi vì điều này". Đúng vậy, giải pháp này đã phá vỡ sự liên quan của mô hình Model-View-Controller, nó có thể dẫn đến việc gọi sai dữ liệu cái mà rất khó để theo dõi, và maintain trong tương lai. Nhưng hãy xem xét giá trị của việc thực hiện nó theo The Wrong Way
:
- Bạn có thể đẩy sự thay đổi này lên staging trong vòng 15 phút.
- Ngoài ra, block này dễ nhớ.
- Khắc phục sự cố Rails này là đơn giản (có thể được giao cho một junior developer).
Step 2. Partials
Sau khi thực hiện The Wrong Way
, tôi cảm thấy tồi về bản thân mình và muốn tách biệt code xấu đó. Nếu sự thay đổi này rõ ràng chỉ là mối liên quan trong lớp Views, tôi có thể refactor đoạn code xấu hổ của tôi thành một partial.
app/views/pages/welcome.slim:
= render :partial => 'shared/projects_list'
app/views/shared/_projects_list.slim:
ul.projects
- Project.where(active: true).each do |project|
li.project = link_to project_path(project), project.name
Nó trông đã tốt hơn một chút. Rõ ràng, chúng ta vẫn đang mắc lỗi của một truy vấn Model trong View, nhưng ít nhất khi một người bảo trì đến sau và thấy một partial kinh khủng của tôi, họ sẽ có một cách giải quyết vấn đề mã Rails cụ thể. Sửa một phần nào đó không thông minh nhưng rõ ràng là luôn luôn dễ dàng hơn so với việc sửa một giải pháp tồi đã cài đặt với nhiều lỗi trừu tượng.
Step 3. Helpers
Helpers trong Rails là một cách để tạo ra một DSL (Domain Specific Language) cho một section của Views. Bạn phải viết lại mã của mình bằng cách sử dụng content_tag
thay vì slim hoặc HTML, nhưng bạn sẽ có được lợi ích khi được phép thao tác các cấu trúc dữ liệu mà không để các developer khác nhìn chằm chằm vào bạn trong 15 dòng mã views.
Nếu tôi được sử dụng helpers ở đây, tôi có thể refactor lại thẻ li
:
app/views/shared/_projects_list.slim:
ul.projects
- Project.where(active: true).each do |project|
= project_list_item(project)
app/helpers/projects_helper.rb:
def project_list_item(project)
content_tag(:li, :class => 'project') do
link_to project_path(project), project.name
end
end
Controllers
Step 4. Chuyển nó tới Controllers
Có lẽ đoạn code khủng khiếp của bạn không chỉ là mối liên hệ của riêng Views. Nếu code của bạn vẫn còn bốc mùi, hãy tìm các truy vấn bạn có thể chuyển từ Views sang Controllers.
app/views/shared/_projects_list.slim:
ul.projects
- @projects_list.each do |project|
= project_list_item(project)
app/controllers/pages_controller.rb:
def welcome
@projects = Project.where(active: true)
end
Step 5. Controller Filters
Lý do rõ ràng nhất để di chuyển code vào before_filter
hoặc after_filter
của Controller là sử dụng cho đoạn code mà bạn đã lặp lại trong nhiều action của Controller. Bạn cũng có thể di chuyển code vào filter của Controller nếu bạn muốn tách riêng mục đích của action của controller khỏi các yêu cầu trên views của bạn.
app/controllers/pages_controller.rb:
before_filter :projects_list
def welcome
end
def projects_list
@projects = Project.where(active:true)
end
Step 6. Application Controller
Giả sử rằng bạn cần đoạn code của bạn để hiển thị trên mỗi trang, hoặc bạn muốn tạo một hàm Controller helper để có thể gọi trong mọi controllers, bạn có thể di chuyển hàm đó vào ApplicationController
. Nếu những thay đổi là chung cho tất cả các trang, bạn cũng có thể nên thay đổi cả layout application của bạn.
app/controllers/pages_controller.rb:
def welcome
end
app/views/layouts/application.slim:
ul.projects
- projects_list.each do |project|
= project_list_item(project)
app/controllers/application_controller.rb:
before_filter :projects_list
def projects_list
@projects = Project.where(active: true)
end
The Models
Step 7. Model
Như phương châm của mô hình MVC đã chỉ ra: Fat Model, Skinny Controllers, và Dumb Views. Chúng ta trông đợi sẽ refactor lại tất cả mọi thứ có thể vào trong Model, và đúng là hầu hết các chức năng phức tạp cuối cùng sẽ trở thành các quan hệ giữa các model và các phương thức trong model. Chúng ta nên tránh định nghĩa các hàm format hoặc views trong Model, nhưng việc chuyển đổi dữ liệu thành các loại dữ liệu khác là được phép.
Trong ví dụ của chúng ta, phần tốt nhất để refactor vào model sẽ là mệnh đề where(active: true)
, mà chúng ta có thể biến thành một scope
. Sử dụng scope
không chỉ giúp cho việc gọi đến nó dễ dàng hơn mà còn giúp chúng ta trong trường hợp chúng ta muốn thêm một thuộc tính mới như deleted
hay outdated
, chúng ta chỉ cần sửa scope này mà không phải tìm tất cả các mệnh đề where
để sửa.
app/controllers/application_controller.rb:
before_filter :projects_list
def projects_list
@projects = Project.active
end
app/models/project.rb:
scope :active, where(active: true)
Step 8. Model Filters
Chúng ta không có mục đích sử dụng cụ thể cho các bộ lọc before_save
hoặc after_save
của Model trong ví dụ này, nhưng bước tiếp theo tôi thường thực hiện là chuyển các chức năng từ các hàm Controller và các hàm Model thành các bộ lọc Model.
Giả sử chúng ta có một thuộc tính khác, num_views
. Nếu num_views > 50
, project sẽ không hoạt động. Chúng ta có thể giải quyết vấn đề này trong View, nhưng thay đổi cơ sở dữ liệu trong View là không phù hợp. Chúng ta có thể giải quyết nó trong Controller, nhưng Controller của chúng ta nên "thin" nhất có thể! Nhưng chúng ta có thể giải quyết nó một cách dễ dàng trong Model.
app/models/project.rb:
before_save :deactivate_if_over_num_views
def deactivate_if_over_num_views
if num_views > 50
self.active = false
fi
end
Note: Bạn nên tránh việc gọi self.save
trong Model filter, vì điều này gây ra sự kiện lưu đệ quy, và tầng thao tác cơ sở dữ liệu của ứng dụng của bạn nên là Controller.
Step 9. Libraries
Đôi khi, chức năng của bạn đủ lớn để có thể chuyển nó thành một thư viện riêng. Bạn có thể muốn di chuyển nó vào một file thư viện bởi vì nó được tái sử dụng ở rất nhiều nơi, hoặc nó đủ lớn mà bạn muốn phát triển nó một cách riêng biệt.
Bạn nên lưu các file thư viện trong thư mục lib/
, nhưng khi chúng phát triển thêm, bạn có thể chuyển chúng thành một RubyGem! Một lợi thế lớn của di chuyển code của bạn vào thư viện là bạn có thể test thư viện riêng biệt với model của bạn.
Dù sao, trong trường hợp của một danh sách Project, chúng ta có thể biện minh cho việc di chuyển scope :active
từ model Project vào một file thư viện và gọi nó lại vào Ruby:
app/models/project.rb:
class Project < ActiveRecord::Base
include Activeable
before_filter :deactivate_if_over_num_views
end
lib/activeable.rb:
module Activeable
def self.included(k)
k.scope :active, k.where(active: true)
end
def deactivate_if_over_num_views
if num_views > 50
self.active = false
end
end
end
Note: phương thức self.included
được gọi khi một lớp Rails Model được cung cấp và truyền vào trong lớp scope như là biến k
.
Kết luận
Trong hướng dẫn refactor Ruby on Rails code này, chúng ta đã thực hiện chưa đến 15 phút và triển khai một giải pháp và đưa nó lên staging để người dùng test, sẵn sàng để được chấp nhận vào bộ chức năng hoặc gỡ bỏ. Khi kết thúc quá trình refactor, chúng ta có một đoạn code mà có thể vượt qua cả quá trình review nghiêm ngặt nhất.
Trong quá trình refactor Rails của riêng bạn, bạn có thể bỏ qua một vài bước nếu bạn tin tưởng vào việc làm như vậy (ví dụ: nhảy từ View tới Controller, hoặc Controller tới Model). Chỉ cần lưu ý về luồng code từ View sang Model.
Đừng sợ code nhìn ngu ngốc. Điều khác biệt giữa các ngôn ngữ hiện đại với các ứng dụng render template CGI cũ không phải là chúng ta làm mọi việc đúng cách trong mọi lần - mà là chúng ta cần thời gian để refactor, tái sử dụng và chia sẻ những nỗ lực của chúng ta.
Link source: https://www.toptal.com/ruby-on-rails/build-dumb-refactor-smart-ruby-on-rails
All rights reserved