【Ruby on Rails】コントローラの共通化の方法
Ruby on Railsにおけるコントローラの共通化の方法をまとめてみたいと思います。
本記事は私の経験則から書いておりますので、間違っていればご指摘頂けますと幸いです。
コントローラの共通化
コントローラの共通化の際には以下の手法を用いることが多いです。
- 継承
- Mixin
例として、商品(本や服)を出品するアクションを作っているとします。(editとupdate以外は割愛してます)
class BooksController < ApplicationConroller
def edit
@book = Book.find(params[:id])
@post_user = current_user
end
def update
@book = Book.find(params[:id])
@post_user = current_user
if @book.update(book_parmas)
redirect_to @book
else
render :edit
end
end
private
def book_params
params.require(:book).permit(
:title,
:author_name,
:price,
:comment
)
end
end
class ClothsController < ApplicationConroller
def edit
@cloth = Cloth.find(params[:id])
@post_user = current_user
end
def update
@cloth = Cloth.find(params[:id])
@post_user = current_user
if @cloth.update(cloth_params)
redirect_to @cloth
else
render :edit
end
end
private
def cloth_params
params.require(:cloth).permit(
:size,
:brand_name,
:price,
:comment
)
end
end
上記の2つのアクションは〇〇_params
以外は共通の処理ですので、共通化できます。
継承
継承を用いる場合は親コントローラで共通のメソッドを定義し、子コントローラごとに異なる処理はオーバーライドするようにします。(差分プログラミングとも呼ばれます)
class Products::BaseController < ApplicationController
def edit
@product = model.find(params[:id])
@post_user = current_user
end
def update
@product = model.find(params[:id])
@post_user = current_user
if @product.update(strong_params)
redirect_to @product
else
render :edit
end
end
private
def strong_params
raise NotImplementedError
end
def model
self.class.name.gsub(/Controller\z/, '').singularize.constantize
end
end
class BooksController < Products::BaseController
private
def strong_params
params.require(:book).permit(
:title,
:author_name,
:price,
:comment
)
end
end
class ClothsController < Products::BaseController
private
def strong_params
params.require(:cloth).permit(
:size,
:brand_name,
:price,
:comment
)
end
end
ストロングパラメタを指定する箇所はモデルごとに異なるので、親クラスでNotImplementedError
をraiseし、子クラスで実装を強制するようにしています。
また、model
メソッドでコントローラ名からモデル名を取得するプライベートメソッドも追加しています。もし、コントローラ名からモデル名が取得できない場合は子クラスでオーバーライドすればOKです。
editやupdateアクションも、 子クラスでsuper
を用いることで新しいインスタンス変数をセットするなどの拡張ができる点がメリットだと思います。
class BooksController < ProductsController
def edit
super
@author = @model.author
end
end
一方で、継承することで、子クラスでは不要なアクションが定義されてしまう場合もあり、継承するのが不適切なシーンもありそうです。(が、不要なメソッドはNotImplementedError
をraiseするようオーバーライドするでもいいような気もしています。)
Mixin
Mixinを用いると、継承とは異なりsuper
を用いた拡張ができなくなり、若干拡張しづらい感じがしています。
実装するならば、アクションを定義したモジュールを作成する感じになるかと思います。
module Products::Editable
def edit
@product = model.find(params[:id])
@post_user = current_user
end
def update
@product = model.find(params[:id])
@post_user = current_user
if @product.update(strong_parmas)
redirect_to @product
else
render :edit
end
end
private
def strong_params
raise NotImplementedError
end
def model
self.class.name.gsub(/Controller\z/, '').singularize.constantize
end
end
class BooksController < ApplicationController
include Products::Editable
private
def strong_params
params.require(:book).permit(
:title,
:author_name,
:price,
:comment
)
end
end
class ClothsController < ApplicationController
include Products::Editable
private
def strong_params
params.require(:cloth).permit(
:size,
:brand_name,
:price,
:comment
)
end
end
拡張する場合は、module側でメソッドを定義してあげる感じになるかなと思います。
module Products::Editable
def edit
@product = model.find(params[:id])
@post_user = current_user
additional_instances
end
private
def additional_instances
nil
end
end
class BooksController < ApplicationController
include Products::Editable
private
def additional_instances
@author = @model.author
end
end
一方で、before_actionなどでインスタンス変数をセットする処理をMixinにして共通化するなどもありますが、これはこれでどのインスタンス変数がセットされているかが追いづらく、includeするMixinが増えるにつれて辛みが増していきそうなので、バランスが重要そうに思います。
参考
Discussion