【Rails】DHH流のコントローラ分割について

コードレビューでコントローラ分割のアドバイスを頂いたのでDHH流のRailsのコントローラの分割について調べました。

DHH流のコントローラ分割とは?

コントローラが元々持っているRESTアクションやデフォルトの5つの機能にはないメソッドを付け加えたいと思ったら、いつだって新しいコントローラを作る。

コントローラはデフォルトのCRUDアクション index 、 show 、 new 、 edit 、 create 、 update 、 destroy のみを使うべきだということです。

DHHはどのようにRailsのコントローラを書くのか | POSTD

ポイントは次の2つです。

  • コントローラのアクションはデフォルトのCRUDアクション(indexshowneweditcreateupdatedestroy)に限定する。
  • CRUDアクション以外のアクションを追加する場合はそのアクションを1つのリソースと見立て、サブコントローラを増やす。

メリット

DHH流のコントローラ分割を行うことでいくつかのメリットがあります。

  1. コントローラの肥大化の防止。
    • 各コントローラ内のアクションはデフォルトCRUDアクションに限定されるので可読性が良い。
    • リソース毎にコントローラが増えるのでのコントローラのは増える。
  2. ルーティングがRESTに基づいてルール化される。
    • RESTfulなルーティングになるので可読性が良い。
    • ルーティング設計が楽になる。RESTアクションとリソース名を決めるだけでOK。

メリットについての詳細は以下のページがとても参考になります。

DHHはどのようにRailsのコントローラを書くのか | POSTD

DHH流のルーティングで得られるメリットと、取り入れる上でのポイント - KitchHike Tech Blog

実装例

usersコントローラ内のフォロー数/フォロワー数を取得するfollowings/followers メソッドをDHH流の方法でコントローラを分割します。 コントローラーを分割するにあたって次の3つを行います。

  1. ルーティングの変更
  2. サブコントローラの追加
  3. view、localeファイル等の修正

ちなみにですが修正前の実装はRailsチュートリアルの下記記事を参照し、実装しています。

Railsチュートリアルのルーティング実装例

Railsチュートリアルのコントローラ実装例

1. ルーティングの変更

memberメソッドでusersリソースに:id 付きでfollowingsfollowersのルーティングを追加しています。

# 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

上記コードのfollowingsfollowers を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コントローラで定義されているfollowingsfollowers メソッドをapp/controllers/users/followings_controller.rbapp/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コントローラにfollowingsfollowers アクションをネストさせているのでコントローラのディレクトリは下記のようになります。

# 変更前
.
└ 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.rbapp/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でディレクトリを指定してファイルを作成する - 時々とおまわり

DHH流ルーティングの導入しようとした際にハマったこと - Qiita

DHH流 コントローラーを分割する - Chiroru's Diary