Runner in the High

技術のことをかくこころみ

コントローラを名前空間で分離して責務の分割をする

Deviseなどを使って、ひとつ以上のスコープを持つアプリケーションを開発している際に、RESTfulなコントローラとビューが1対1対応をしていると、コントローラがどうしても複数のスコープが絡みついた見通しの悪いコードになりがちである。

たとえば、ECカートを実装する例を考えるとする。まず、ChargesControllerがあって、newアクションとcreateアクションはUserスコープによって決済を行う、いわゆるカートの画面を表示にまつわるもの。そして、showアクションはAdminスコープのみでアクセスできるよう認証を実施し、決済情報の詳細を見れるものである。

class ChargesController < ApplicationController 
  def show
    authenticate_admin!
    ... 
  end
  
  def new
    ...
  end
end

この段階ではコントローラはまだ小さいので、さほど問題は感じない。だが、開発をすすめていくうちに、ユーザーによる決済の執行の過程で、エラーが起きた際に、特別な処理をしなくてはならない必要性が出てきたため、コントローラの中にrescue_fromブロックを用意し、DRIな実装を試みようとした。

class ChargesController < ApplicationController
  rescue_from do
    # ここでnew/createでの例外を拾う
  end

  def show
    authenticate_admin!
    ...
  end

  def new
    # 例外が起こる可能性がある処理
  end
    
  def create
    # 例外が起こる可能性のある処理
  end
end  

このようなコードを書くと、今度はshowアクションの中にある例外も、同じくrescue_fromの中で拾われてしまい、Adminスコープとして必要な処理とUserスコープとして必要な処理がコントローラの中に混在してくことになる。また、authenticate_admin!のような認証系の処理も、今例の短いコードであればよいが、コードの量が多くなればなるほど、ひと目ではどのアクションがどのユーザースコープに限定されたものであるのか理解しづらくなる。

こうしたコントローラの問題は、アプリケーションが肥大化した際にコントローラ内部のスコープの責務がすぐにはわかりづらくなり、スケールする際にバグを生みやすいという点である。このような責務の混在を解消するためのアプローチのひとつとして、コントローラを名前空間で分離し、責務の分割を図るという方法が有効である。

実装

Railsでは、ディレクトリの階層構造が名前空間を作るため、ディレクトリ構造は以下のようにUsersAdmins以下にそれぞれのChargesController.rbを作成する。

もしも、すでにDeviseでscoped_viewtrueにしてオーバライド用のコントローラを生成していれば、controllers以下にはスコープ名のディレクトリが存在しているはずなので、その中にコントローラを作ればよい。

- app
  + assets
  + channels
  - controllers
    - admins
      ...
      * charges_controller.rb
    - users
      ...
      * charges_controller.rb
    * application_controller.rb
  ...

コントローラへ名前空間を付与し、RESTfulに従いながらスコープに応じて必要なアクションを実装していく。

class Admins::ChargesController < ApplicationAontroller
  def show
    ...
  end
end
class Users::ChargesController < ApplicationController
  rescue_from do
    ... 
  end
  
  def create
    ...
  end
  
  def new
    ...
  end
end

上記のようにコントローラへ名前空間が付与されたため、ルーティングでは以下のコードのようにscope module: xxxなどを使う必要がある。

Rails.application.routes.draw do
  scope module: :users do
    resources :charges, only: %i(new create) 
  end
  
  scope module: :admins do
    resource :charges, only: %i(show) 
  end
end

これで、もとのChargesControllerは、Users::ChargesControllerAdmins::ChargesControllerに分割され、ひとつのコントローラ内の責務を適切に分割することができた。

まとめ

このテクニックは、実際に業務で遭遇した問題と、POSTDで寄稿された「DHHはどのようにコントローラを書くのか」という記事からインスピレーションを経た。このPOSTDの記事はいかにRESTfulのルールを維持しながら、Railsのコントローラを書くかというベストプラクティスのひとつであったが、その方策はDeviseを使う場合でも応用できる。

肥大化したRailsアプリケーションのリファクタリングのための最初の一歩は、(もしまだやっていなければ)きれいなコントローラを書くことだ。そのために、こうしたベストプラクティスをできるだけ多く取り入れて、気持ちの良い開発をしていければいいと思う。

その他

ひとつ思いつく別の解決策として、StandardErrorを継承したUserErrorAdminErrorなどのような独自例外を定義し、それぞれのスコープに関連するアクションからrescueブロックを用いて更に例外を投げ直すという手も考えられる。これであれば、コントローラの分割をする必要はない。だが、いずれにしてもアプリケーションが肥大化するにあたっては、コントローラの分割が最もよい手法であると思う。