🐄

【Ruby on Rails】コントローラの共通化の方法

2023/05/01に公開

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が増えるにつれて辛みが増していきそうなので、バランスが重要そうに思います。

参考

https://qiita.com/sumin/items/0ca96afd4bda8c1eab00

Discussion