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では、ディレクトリの階層構造が名前空間を作るため、ディレクトリ構造は以下のようにUsers
とAdmins
以下にそれぞれのChargesController.rb
を作成する。
もしも、すでにDeviseでscoped_view
をtrue
にしてオーバライド用のコントローラを生成していれば、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::ChargesController
とAdmins::ChargesController
に分割され、ひとつのコントローラ内の責務を適切に分割することができた。
まとめ
このテクニックは、実際に業務で遭遇した問題と、POSTDで寄稿された「DHHはどのようにコントローラを書くのか」という記事からインスピレーションを経た。このPOSTDの記事はいかにRESTfulのルールを維持しながら、Railsのコントローラを書くかというベストプラクティスのひとつであったが、その方策はDeviseを使う場合でも応用できる。
肥大化したRailsアプリケーションのリファクタリングのための最初の一歩は、(もしまだやっていなければ)きれいなコントローラを書くことだ。そのために、こうしたベストプラクティスをできるだけ多く取り入れて、気持ちの良い開発をしていければいいと思う。
その他
ひとつ思いつく別の解決策として、StandardError
を継承したUserError
やAdminError
などのような独自例外を定義し、それぞれのスコープに関連するアクションからrescue
ブロックを用いて更に例外を投げ直すという手も考えられる。これであれば、コントローラの分割をする必要はない。だが、いずれにしてもアプリケーションが肥大化するにあたっては、コントローラの分割が最もよい手法であると思う。