Ruby
Rails
mixin

Railsアプリケーションでmixinを利用する

概要

Rubyの特徴であるmixinをどのようにRailsアプリケーションで活用したときに、どのようなメリットがあるのか考える。

mixinとは

moduleをclassでincludeすることにより、moduleのメソッドをclassで使用できるようにすること。

module TestModule
  def test
    "test"
  end
end

class TestClass
  include TestModule
end

Test.new.test
=> "test"

Railsアプリケーションでの利用

業務ロジックをmoduleとして定義し、Controllerでincludeする

controllers/concerns/user_client.rb
module UserClient
  extend ActiveSupport::Concern

  included do
    helper_method :user
  end

  def user
    @_user ||= User.find(params[:user_id])
  end
end
controllers/concerns/user_profile_client.rb
module UserProfileClient
  extend ActiveSupport::Concern
  include UserClient

  included do
    helper_method :user_profile
  end

  def user_profile
    @_user_profile ||= UserProfile.find(user.id)
  end
end
controllers/users_controller.rb
class UsersController < ApplicationController
  include UserClient
  include UserProfileClient

  def show
  end
end
views/users/show.erb.html
<%= user.name %>
<%= user_profile.age %>

mixin利用のメリット

Fat Controllerからの脱却

従来の実装では、Controllerに処理の記述が多くなってしまう。
mixinを利用することで、moduleとして切り出すことができる。

controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:user_id])
    @user_profile = UserProfile.find(@user.id)
  end
end

Fat Modelからの脱却

Fat Controllerから脱却手段としては、Modelに業務ロジックを記載する方法もある。
別モデルの処理の記述が入ったりして、特定モデルの処理の記述が多くなってしまう。
複数のモデルを参照する処理をmoduleに切り出すことで、特定モデルの記述を最小限にする。

models/users.rb
class UserModel < ActiveRecord::Base
  # relationで取得するのがベストだが、あくまでmixinとの比較のため今回はメソッド定義の形とする。
  def user_profile
    UserProfile.find(id)
  end
end

処理順を気にしなくてよくなる

Fat Controllerの例では、userを生成してから、user_profileを生成するという処理順を気にする必要がある。

処理順を逆にするとエラーになる。

controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @user_profile = UserProfile.find(@user.id) # @userはnilなので、idメソッドをコールできずにエラーとなる。
    @user = User.find(params[:user_id])
  end
end

一方で、mixinではmodule間の依存関係に気をつければよく、処理順を気にする必要はない。

controllers/concerns/user_profile_client.rb
module UserProfileClient
  extend ActiveSupport::Concern
  # 依存関係を定義
  include UserClient

  included do
    helper_method :user_profile
  end

  def user_profile
    # userメソッドをコールしたときに処理が実行されるので、何らかの事前処理は不要。
    @_user_profile ||= UserProfile.find(user.id)
  end
end

Controllerのbefore_actionの煩わしさから解放される

特定のUser情報だけ必要なdef showと、すべてのUser情報が必要なdef indexを定義したとき、
それぞれのメソッドで必要な情報を考慮して、before_actionにonly指定をする必要がある。
only指定をしない場合、showですべてのUser情報取得の処理が実行され、無駄な処理をしてしまう。

controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :user, only: :show
  before_action :users, only: :index

  def show
  end

  def index
  end

  private

  def user
    @user = User.find(params[:user_id])
  end

  def users
    @users = User.all
  end
end

一方で、mixinを利用したとき、moduleのメソッドがコールされたときに処理が実行されるので、moduleで必要となるメソッドを定義しておいても無駄な処理は発生しない。必要なときに必要な処理量で、必要な情報を取得できる。

controllers/concerns/user_client.rb
module UserClient
  extend ActiveSupport::Concern

  included do
    helper_method :user
    helper_method :users
  end

  def user
    @_user ||= User.find(params[:user_id])
  end

  def users
    @_users ||= User.all
  end
end
controllers/concerns/user_profile_client.rb
module UserProfileClient
  extend ActiveSupport::Concern
  include UserClient

  def show
  end

  def index
  end
end
views/users/show.erb.html
<%= user.name %>
<%# usersをコールしない限り、usersの処理は実行されないが、必要な時はコールできる %>
<%= users.size %>

views/users/index.erb.html
<%= users.name %>

論点

moduleで定義したメソッドをviewで使用する場合、helper_methodとして定義するか、helper側でcontrollerにdelegateするか

paramsはhelper側でdelegateしている
https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/controller_helper.rb#L16