コードレビューでコントローラ分割のアドバイスを頂いたのでDHH流のRailsのコントローラの分割について調べました。
DHH流のコントローラ分割とは?
コントローラが元々持っているRESTアクションやデフォルトの5つの機能にはないメソッドを付け加えたいと思ったら、いつだって新しいコントローラを作る。
コントローラはデフォルトのCRUDアクション index 、 show 、 new 、 edit 、 create 、 update 、 destroy のみを使うべきだということです。
DHHはどのようにRailsのコントローラを書くのか | POSTD
ポイントは次の2つです。
- コントローラのアクションはデフォルトのCRUDアクション(
index
、show
、new
、edit
、create
、update
、destroy
)に限定する。 - CRUDアクション以外のアクションを追加する場合はそのアクションを1つのリソースと見立て、サブコントローラを増やす。
メリット
DHH流のコントローラ分割を行うことでいくつかのメリットがあります。
- コントローラの肥大化の防止。
- 各コントローラ内のアクションはデフォルトCRUDアクションに限定されるので可読性が良い。
- リソース毎にコントローラが増えるのでのコントローラのは増える。
- ルーティングがRESTに基づいてルール化される。
- RESTfulなルーティングになるので可読性が良い。
- ルーティング設計が楽になる。RESTアクションとリソース名を決めるだけでOK。
メリットについての詳細は以下のページがとても参考になります。
DHHはどのようにRailsのコントローラを書くのか | POSTD
DHH流のルーティングで得られるメリットと、取り入れる上でのポイント - KitchHike Tech Blog
実装例
usersコントローラ内のフォロー数/フォロワー数を取得するfollowings
/followers
メソッドをDHH流の方法でコントローラを分割します。
コントローラーを分割するにあたって次の3つを行います。
- ルーティングの変更
- サブコントローラの追加
- view、localeファイル等の修正
ちなみにですが修正前の実装はRailsチュートリアルの下記記事を参照し、実装しています。
1. ルーティングの変更
memberメソッドでusersリソースに:id
付きでfollowings
とfollowers
のルーティングを追加しています。
# routes.rb # 変更前 Rails.application.routes.draw do ... resources :users, only: %i[index show] do member do # 変更する get :followings, :followers end resource :friendships, only: %i[create destroy] end end
# rails routes 実行結果 # 変更前 $ rails routes Prefix Verb URI Pattern Controller#Action ... users GET /users(.:format) users#index user GET /users/:id(.:format) users#show followings_user GET /users/:id/followings(.:format) users#followings # 変更する followers_user GET /users/:id/followers(.:format) users#followers # 変更する user_friendships DELETE /users/:user_id/friendships(.:format) friendships#destroy POST /users/:user_id/friendships(.:format) friendships#create
上記コードのfollowings
とfollowers
をusersリソースのサブリソースに見立ててresourcesメソッドで:user_id
付きのルーティングに変更します。
次の2つがポイントになります。
- onlyオプションで追加するルーティングを必要な
index
アクションのみに限定 - moduleオプションでusersコントローラにネストする形でルーティングを追加
# routes.rb # 変更後 Rails.application.routes.draw do resources :users, only: %i[index show] do resources :followings, only: [:index], module: 'users' # サブリソースに変更 resources :followers, only: [:index], module: 'users' # サブリソースに変更 resource :friendships, only: %i[create destroy] end end
# rails routes 実行結果 # 変更後 $ rails routes Prefix Verb URI Pattern Controller#Action users GET /users(.:format) users#index user GET /users/:id(.:format) users#show user_followings GET /users/:user_id/followings(.:format) users/followings#index # usersコントローラにネストされている user_followers GET /users/:user_id/followers(.:format) users/followers#index # usersコントローラにネストされている user_friendships DELETE /users/:user_id/friendships(.:format) friendships#destroy POST /users/:user_id/friendships(.:format) friendships#create
2. サブコントローラの追加
usersコントローラで定義されているfollowings
、followers
メソッドをapp/controllers/users/followings_controller.rb
、app/controllers/users/followers_controller.rb
としてサブコントローラに切り出します。
# app/controllers/users_controller.rb # 変更前 class UsersController < ApplicationController def index @users = User.with_attached_avatar.order(:id).page(params[:page]) end def show @user = User.find(params[:id]) end def followings # サブコントローラに切り出す @followings = User.find(params[:id]).followings.order(:id).page(params[:page]) end def followers # サブコントローラに切り出す @followers = User.find(params[:id]).followers.order(:id).page(params[:page]) end end
ルーティング変更時にmodule
オプションを使いusersコントローラにfollowings
、followers
アクションをネストさせているのでコントローラのディレクトリは下記のようになります。
# 変更前 . └ app/ └ controllers/ └ users_controller.rb # 変更後 . └ app/ └ controllers/ ├ users_controller.rb └ users/ ├ followings_controller.rb └ followers_controller.rb
サブディレクトリを指定して新たにコントローラを作成する方法は別記事に書いたので参考にしてください。
rails g controllerでディレクトリを指定してファイルを作成する - 時々とおまわり
app/controllers/users_controller.rb
の不要なメソッドを削除し、サブコントローラとしてapp/controllers/users/followings_controller.rb
、app/controllers/users/followers_controller.rb
を追加します。
# app/controllers/users_controller.rb # followings、followersメソッドを削除 class UsersController < ApplicationController def index @users = User.with_attached_avatar.order(:id).page(params[:page]) end def show @user = User.find(params[:id]) end end
# app/controllers/users/followings_controller.rb # 追加したサブコントローラ class Users::FollowingsController < ApplicationController def index @followings = User.find(params[:user_id]).followings.order(:id).page(params[:page]) end end
# app/controllers/users/followers_controller.rb # 追加したサブコントローラ class Users::FollowersController < ApplicationController def index @followers = User.find(params[:user_id]).followers.order(:id).page(params[:page]) end end
3. view、localeファイル等の修正
ルーティング、コントローラを修正したことで関連するview、localeファイルの修正が必要な場合があります。 環境によって異なるので今回は割愛します。 作成したサンプルアプリのリポジトリを参考にしてください。
Railsでユーザーフォローを作る by karlley · Pull Request #5 · karlley/fjord-books_app
感想
初めてコントローラの分割を行いましたが、実装してみると確かに可読性や保守性が高まるなぁと感じました。 Railsチュートリアルを参考に実装していましたが、考え方でこんなにも実装が変わるという知見も得ることができたのでとても良い機会になりました。 今後大きなアプリに対してメソッドを追加したくなったら、一度この記事に立ち返って実装方針についてしっかり考えた上で実装していこうと思います。
参照
DHHはどのようにRailsのコントローラを書くのか | POSTD
DHH流のルーティングで得られるメリットと、取り入れる上でのポイント - KitchHike Tech Blog
第14章 ユーザーをフォローする - Railsチュートリアル
rails g controllerでディレクトリを指定してファイルを作成する - 時々とおまわり