12/08/2018, 14:07

Thiết kế Routes và Controllers

h1. *ROUTES* Chỉ cần nhìn vào file routes.rb cũng có nhiều thứ để nói về chất lượng một Rails app. Cứ nghĩ mà xem, routes.rb là nơi duy nhất thể hiện toàn bộ application của bạn về mặt chức năng. Vì lý do đó mà thiết kế routes càng có giá trị về thông tin (informative) thì càng tốt. Bằng cách ...

h1. *ROUTES*

Chỉ cần nhìn vào file routes.rb cũng có nhiều thứ để nói về chất lượng một Rails app. Cứ nghĩ mà xem, routes.rb là nơi duy nhất thể hiện toàn bộ application của bạn về mặt chức năng. Vì lý do đó mà thiết kế routes càng có giá trị về thông tin (informative) thì càng tốt. Bằng cách này, developers có thể hiểu specs tốt hơn và viết code tốt hơn, theo 4 tiêu chí: clear, concise, readable and maintainable.

h2. Lên ý tưởng từ Routes trước khi implement features mới

Làm việc theo route-first approach thì quy trình sẽ như thế này: Giả sử muốn thêm feature subscribe cho Blog app, có thể export subscription data ra file CSV. Trước khi viết bất cứ một đoạn code nào, phác thảo ý tưởng ra routes.rb trước đã, cơ bản nó trông thế này:

resources :blogs, only: [:index, :show], shallow: true do
  resources :posts, only: [:show] do
    resources :comments, only: [:show, :new, :create]
  end
  member do
    post 'subscribe'
    get 'export_subscription', constraints: { format: 'csv' }
  end
end

Sau 1 thời gian brainstorming, specs có thể thay đổi với nhiều tính năng hơn:

resources :blogs, only: [:index, :show], shallow: true do
  # ...
  member do
    post 'subscribe'
    post 'unsubscribe'
    get 'change_subscription'
    post 'update_subscription_settings'
    get 'subscription_info', constraints: { format: 'json' }
    get 'export_subscription', constraints: { format: 'csv' }
  end
end

Khi mà ta có xu hướng thêm vào nhiều hơn 3 actions cho cùng 1 resource, là lúc dừng lại và cân nhắc sự cần thiết của một resource route.

resources :blogs, only: [:index, :show], shallow: true do
  # ...
  resource :subscription, except: [:new]
end

Ít nhất 3/4 tiêu chí đã nêu ở phần trên đã được đảm bảo. Chưa cần đề cập đến JSON hay CSV vì ta có thể xử lý nó trong controller Subscriptions#show. Tiết kiệm được kha khá thời gian xử lý đống view, controller nếu thiết kế như ban đầu. Thậm chí để xử lý URL redirect không cần xử dựng tới 2 dòng code sau:

get '/blogs/:id/subscription_info',   to: redirect('/blogs/%{id}/subscription.json'), constraints: { format: 'json' }
get '/blogs/:id/export_subscription', to: redirect('/blogs/%{id}/subscription.csv'),  constraints: { format: 'csv'  }
h2. Only and except

Nên sử dụng ít API nhất có thể mà vẫn đảm bảo hệ thống về mặt chức năng. Xem xét 2 ví dụ dưới đây:

resources :products

resources :products, only: [:index, :show]

Trong 2 ví dụ trên, code đều đúng, chức năng vẫn đảm bảo (và dòng đầu thì ngắn gọn hơn). Tuy vậy, sử dụng ví dụ thứ 2 nếu hàm ý ở đây là chỉ cần hiển thị products ra thôi.

h2. Sử dụng nhiều controller

Với beginers, cách của họ thường là 1 controller / 1 model. Nhưng Rails khuyến khích 'One perspective per controller'.

Lấy ví dụ 1 E-commerce app, ta nên có 1 controller riêng phụ trách quản lý products, dành cho admin (admin perspective) và 1 controller riêng để duyệt products cho users/guests (public users perspective).

Xem xét 2 ví dụ dưới đây:

# bad
resources :products
resources :orders do
  get :history, on: :collection
  post :confirm, on: :member
end

# good
resources :products, only: [:index, :show]
namespace :my do
  resources :orders, only: [:index, :show]
end
namespace :shop do
  resources :products, only [:index, :show, :new, :create, :edit, :update]
  resources :orders, only: [:index, :show] do
    post :confirm, on: :member
  end
end

Về chức năng thì 2 ví dụ trên không khác nhau.

Ở ví dụ 1, ta sẽ có các controllers và actions đảm nhận chức năng của 2 roles (admin & public users). Hậu quả: khả năng sử dụng if/else trong view, dùng action name không phải Restful action name (vd: history).

Ở ví dụ 2, routes cho ta khả năng mô tả một cách đơn giản user stories. Khi đó, ở controller, sử dụng strong_params mang ý nghĩa nhiều hơn, quy định rõ hơn controller nào được thay đổi gì ở model.

h1. *CONTROLLERS & ACTIONS*
h2. Không logic trong controllers

Không logic trong controllers có vẻ không được đúng lắm nhỉ. Chính xác thì ý của câu đó là: không business logic, chỉ control logic.

Control logic? Tức là controler chỉ làm nhiệm vụ chỉ ra view nào để render hay redirect mà thôi. Có thể dùng before_action để kiểm tra quyền của users có được phép truy cập đến resource đó không.

Được lợi gì khi không có business logic: thinner controller, viết test cho controller dễ hơn.

Tóm lại controller chỉ nên bám theo 1 trong 2 forms này:

    # reading type operation
    def index
      @resource = Resource.all
    end
    # create, update, delete type operations
    def create
      if @resource.save
        redirect_to somewhere_path
      else
        render :new
      end
    end
h2. One thing, one line per controller and action

Mỗi controller và action nên rõ ràng về việc chúng làm việc với resource nào. Lấy ví dụ 1 app quản lý project, yêu cầu là khi tạo 1 project tạo luôn 3 tasks cho project đó.

Cách hợp lý là nên chuẩn bị 2 controllers

# config/routes.rb
resources :project, only: [:new, :create, :show] do
  resources :tasks, only [:create, :edit, :update]
end

Và ở trong controller

# app/controllers/projects_controller.rb
class ProjectsController < ActiveRecord::Base
  def create
    @project = Project.create(project_params)
  end
  private
  def project_params
    params.require(:project).permit(:name, task_attributes: [:description, :assignee_id])
  end
end
# app/controllers/tasks_controller.rb
class TasksController < ActiveRecord::Base
  before_action :prepare_project
  def create
    @task = @project.tasks.create(task_params)
  end
  private
  def prepare_project
    @project = Project.find(params[:project_id])
  end
  def task_params
    params.require(:task).permit(:description, :assignee_id)
  end
end

Có vài chú ý như sau:

  1. Phân định giữa actions với methods bằng private
  2. Dùng before_action để set parent objects, đặc biệt khi dùng nested resource
  3. Sử dụng nested_attributes_for chỉ để làm ví dụ, không được khuyến khích, chi tiết xem tại: http://guides.rubyonrails.org/form_helpers.html#building-complex-forms
  4. DRY code nếu cần thiết
h2. Khi nào dùng before_action

Lưu ý số 2 bên là 1 điển hình nên dùng before_action, ngoài ra còn nên dùng trong trường hợp control redirection rules.

Xem ví dụ sau

namespace :my do
  resource :subscription, only: [:show :new, :create, :edit, :update, :destroy]
end

Yêu cầu:

  • Chuyển users đến trang new subscription nếu không có active subscription nào (tất nhiên là trừ trường hợp action là new/create)
  • Chuyển users đến trang show subscription nếu đã subscribed mà lại truy cập vào new/create

Implement controller thế này

class My::SubscriptionsController < ApplicationController
  before_action :already_subscribed, only: [:new, :create]
  before_action :not_subscribed_yet, only: [:show, :edit, :update, :destroy]
  # actions go here
  private
  def already_subscribed
    redirect_to my_subscription_path if current_user.subscription.active?
  end
  def not_subscribed_yet
    redirect_to new_subscription_path unless current_user.subscription.active?
  end
end
h1. *Mẹo tìm kiếm Routes trong Rails 5*

rake routes liệt kê tất cả routes của app. Nếu app ngày càng to, số lượng routes nhiều lên, tìm kiếm routes cần thiết sẽ khó khăn hơn, nhưng có thể dùng grep

rake routes | grep PATTERN

$ rake routes | grep products
Prefix       Verb   URI Pattern                   Controller#Action
products      GET    /products(.:format)           products#index
              POST   /products(.:format)           products#create

Với phiên bản 5, rails cung cấp một công cụ mạnh mẽ rails routes

# Tìm theo controller (case insensitive)
$ rails routes -c users
       Prefix Verb   URI Pattern                   Controller#Action
wishlist_user GET    /users/:id/wishlist(.:format) users#wishlist
        users GET    /users(.:format)              users#index
              POST   /users(.:format)              users#create

# namespace thì
$ rails routes -c admin/users
         Prefix Verb   URI Pattern                     Controller#Action
    admin_users GET    /admin/users(.:format)          admin/users#index
                POST   /admin/users(.:format)          admin/users#create

# Hoặc thế này cũng được
$ rails routes -c Admin::UsersController
         Prefix Verb   URI Pattern                     Controller#Action
    admin_users GET    /admin/users(.:format)          admin/users#index
                POST   /admin/users(.:format)          admin/users#create

# Match bất cứ PATTERN nào
$ rails routes -g wishlist
       Prefix Verb URI Pattern                   Controller#Action
wishlist_user GET  /users/:id/wishlist(.:format) users#wishlist

$ rails routes -g POST
    Prefix Verb URI Pattern            Controller#Action
           POST /users(.:format)       users#create
           POST /admin/users(.:format) admin/users#create
           POST /products(.:format)    products#create
0