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
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