Thiết kế Routes và Controllers
Bài đăng này đã không được cập nhật trong 8 năm
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
và
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
và
# 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:
- Phân định giữa actions với methods bằng
private
- Dùng
before_action
để set parent objects, đặc biệt khi dùng nested resource - 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 - 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
All rights reserved