0

Refactor Ruby on Rails Code

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:

  1. Bạn có thể đẩy sự thay đổi này lên staging trong vòng 15 phút.
  2. Ngoài ra, block này dễ nhớ.
  3. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí