Concernとは
Concernとは「関心事」という意味で、Railsではコントローラやモデルの一部の概念や機能を実装するモジュールのことを指します。Concernを用いることで、ある特定の概念や機能をコントローラやモデルから切り出すことができ、複数のコントローラやモデル間で使い回すことができます。クラス、モジュール間のincludeやextendの説明はここでは省きます。
スコープ
以下のようにPhotoモデルとVideoモデルに共通のスコープが定義されていたとします。
class Photo < ApplicationRecord
scope :display_member_only, -> { where(member_only: true) }
end
class Video < ApplicationRecord
scope :display_member_only, -> { where(member_only: true) }
end
このとき、以下のようにすることでスコープを共通処理として切り出すことができます。
module CommonModule
def self.included(base)
base.class_eval do
scope :display_member_only, -> { where(member_only: true) }
end
end
end
そしてCommonModuleをincludeすることでこのdisplay_member_onlyを使うことができます。
class Photo < ApplicationRecord
include CommonModule
end
class Video < ApplicationRecord
include CommonModule
end
irb(main):002:0> Video.display_member_only
...(省略)
irb(main):002:0> Photo.display_member_only
...(省略)
# display_member_onlyスコープが使える
そして、ActiveSupport::Concernを使うと、先ほどのモジュール内を以下のように書くことができます。
module CommonModule
extend ActiveSupport::Concern
included do
scope :display_member_only, -> { where(member_only: true) }
end
end
ActiveSupport::Concernを使うことで「def self.included(base) ... end」としていた部分が「included do ... end」に置き換わりました。こちらの方が簡潔に記述できそうですね。
クラスメソッド、インスタンスメソッド
以下のようにクラスメソッドとインスタンスメソッドがモデルに追加されたとしましょう。
class Photo < ApplicationRecord
class << self
def order_by_latest
order(released_at: :desc)
end
end
def title_with_released_at
"#{title}【#{released_at}】"
end
end
class Video < ApplicationRecord
class << self
def order_by_latest
order(released_at: :desc)
end
end
def title_with_released_at
"#{title}【#{released_at}】"
end
end
これらの同じ処理をmoduleに切り出すと以下のように書くことができます。
module CommonModule
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def order_by_latest
order(released_at: :desc)
end
end
def title_with_released_at
"#{title}【#{released_at}】"
end
end
スコープのときと同様、各モデルからCommonModuleをincludeすることによってこれらのメソッドが使えます。
base.extendあたりの書き方は以下の記事が参考になるかと思います。
irb(main):002:0> Video.order_by_latest
...(省略)
irb(main):002:0> Photo.first.title_with_released_at
=> "photo1【2022-04-01】"
そして、ActiveSupport::Concernを使うと以下のように書けます。
module CommonModule
extend ActiveSupport::Concern
module ClassMethods
def order_by_latest
order(released_at: :desc)
end
end
def title_with_released_at
"#{title}【#{released_at}】"
end
end
ActiveSupport::Concernをextendすることで「def self.included(base) ... end」を剥がすことができます。ちなみにクラスメソッドのorder_by_latestは以下のようにも書くことができます。
module CommonModule
extend ActiveSupport::Concern
class_methods do
def order_by_latest
order(released_at: :desc)
end
end
def title_with_released_at
"#{title}【#{released_at}】"
end
end
「module ClassMethods ... end」を「class_methods do ... end」として書く方法です。
こちらの方が直感的に読めるイメージです。
まとめ
スコープとクラスメソッド、インスタンスメソッドをActiveSupport::Concernを使って共通モジュールとしてまとめたものが以下になります。
module CommonModule
extend ActiveSupport::Concern
# スコープ
included do
scope :display_member_only, -> { where(member_only: true) }
end
# クラスメソッド
class_methods do
def order_by_latest
order(released_at: :desc)
end
end
# インスタンスメソッド
def title_with_released_at
"#{title}【#{released_at}】"
end
end
モデル間での共通処理がある場合は、処理の肥大化や重複を避けるためにも積極的に使っていきたいです。