🐘

ActiveSupport::Concern の使い方

2024/01/14に公開

概要

Rails では ActiveSupport::Concern を使ってモジュールを作成しているのをよくみますね。

でも module のメソッドについてあまり理解せずに使っている人も多いかと思います。

今回は そもそも Ruby での module の扱い方法に触れ、
ActiveSupport::Concern を使うとどのように書き方が変わるのかを説明します。

環境

ruby '3.1.2'
'rails', '~> 7.0.8'

ActiveSupport::Concern を使わずに module を使用する

インスタンスメソッドを実行する

モジュールはこちら。

app/models/concerns/m.rb
module M
  def aaa
    "aaa です"
  end
end

クラスにインクルードします。

app/models/m.rb
class C
  include M

  def ddd
    "ddd です"
  end
end

rails console での実行結果。
モジュールで定義したaaa メソッドも クラスで定義したdddメソッドも、
インスタンスメソッドとして実行できていますね。

rails console
> C.new.aaa
=> "aaa です"

> C.new.ddd
=> "ddd です"

クラスメソッドを実行する

モジュールに特異メソッドを定義しても、インクルード先のクラスでは実行できない。

モジュールがこちら

app/models/concerns/m.rb
module M
  def self.aaa
    "aaa です"
  end
end

モジュール M では、クラスメソッドのように実行できます。
しかし、インクルード先の C では、
「そんなメソッド定義されてねーよ」とエラーしてしまいます。

rails console
> M.aaa
=> "aaa"

> C.aaa
(irb):90:in `<main>': undefined method `aaa' for C:Class (NoMethodError)

インクルード先のクラスでモジュールの特異メソッドを実行する方法

主に以下の2つがあります。

  • extend で mix-in する
  • モジュールの読込み時に特異メソッドとして定義する

extend で mix-in する

モジュールではインスタンスメソッドを定義します。

app/models/concerns/m.rb
module M
  def aaa
    "aaa です"
  end
end

定義したモジュールのメソッドを、特異メソッド(クラスメソッド)として実行できるように、
extend で mix-in をする

app/models/c.rb
class C
  extend M

  def ddd
    "ddd です"
  end
end

これで、モジュールのメソッドは特異メソッドとして、
クラスで定義しているインスタンスメソッドはそのままインスタンスメソッドとして実行できます。

rails console
> C.aaa
=> "aaa です"

> C.new.ddd
=> "ddd です"

モジュールの読込み時に特異メソッドとして定義する

先程の例では、モジュールに定義した普通のメソッドと、特異メソッドの両方をそのまま使うことはできませんよね。

インクルード先では、
モジュールで定義したメソッドはインスタンスメソッドとして、
特異メソッドはクラスメソッドとして実行したいはずです。

そんなときは以下のように書きます。

app/models/concerns/m.rb
module M
  def aaa
    "aaa です"
  end

  module ClassMethods
    def bbb
      "bbb です"
    end
  end

  def self.included(base)
    base.extend ClassMethods
  end
end

モジュール内でモジュールを定義しているのは気持ち悪い気がしますが、
その定義したモジュールを、インクルード先で extend するようにしています。

extend は先ほど説明した通り、モジュールで定義したメソッドをクラスメソッドとして読み込ませるものでしたね。

  def self.included(base)
    base.extend ClassMethods
  end

クラスの方では include で mix-in します。

app/models/c.rb
class C
  include M

  def ddd
    "ddd です"
  end
end
rails console
> C.new.aaa
=> "aaa です"

> C.bbb
=> "bbb です"

> C.new.ddd
=> "ddd です"

ちらっと出てきましたが、included do ~ end は、 C クラスにインクルードされた直後に実行されます。

module M
  def self.included(base)
    base.class_eval do
      p "include されました"
    end
  end
end
class C
  include M
end
rails console
> C
"include されました"
=> C

ActiveSupport::Concern を使って module を使用する

今までのことを整理すると、
module で定義したメソッドをインスタンスメソッドとして、
特異メソッドをクラスメソッドとして実行するには、以下のようなコードになります。

module M
  def aaa
    "aaa です"
  end

  module ClassMethods
    def bbb
      "bbb です"
    end
  end

  def self.included(base)
    base.extend ClassMethods
  end
end
class C
  include M
end

ここまでのことを理解していないと、パッと見てもちょっと分かりづらいですよね。

これを、ActiveSupport::Concern を使うと、以下のようになります。

module M
  extend ActiveSupport::Concern

  def aaa
    "aaa です"
  end

  class_methods do
    def bbb
      "bbb です"
    end
  end
end
class C
  include M
end

class_methods メソッドのブロック内でメソッドを定義することで、
インクルード先でクラスメソッドとして使用することができます。
かなりスッキリと見やすくなったのではないでしょうか。

Rails でよく見かける Concern

Rails でよく見るのは、モデルの validation や association を共通化する例ですね。
has_manyscope などを included do ~ end で囲っているのをよく見ます。

module HogeModelModule
  extend ActiveSupport::Concern
  
  included do
    has_many :comments
    
    scope :active, -> { where(is_active: true) }
  end 
end

Discussion