📖

ActiveRecord::Concern使ってインスタンスメソッドを特異メソッドとして追加できる仕組み

2022/01/08に公開

はじめに

ActiveRecord::Concernをextendしたモジュールをクラスやモジュールにincludeすると、名前空間ClassMethods内で定義されたインスタンスメソッドが特異メソッドとして使用可能になります。本記事ではこの処理を担う部分を解説したいと思います。

module M
  extend ActiveRecord::Concern

  module ClassMethods
    def c_method
      'c_method'
    end
  end
end

class MyClass
  include M
end

# インクルードしたモジュール内の::ClassMethodsで定義されたインスタンスメソッドが
# クラスメソッド(特異メソッド)として呼び出し可能
MyClass.respond_to?(:c_method) #=> true

ActiveRecord::Concern内の該当箇所

module ActiveRecord
  module Concern
     #..(省略)..
    def append_features(base)
            # ..(省略)..
            # モジュール内で定義されている定数ClassMethodsをextendすることで特異メソッドとして定義
      base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
    end
  end
end

Module#append_features

append_featureModule#include の実態であり、処理を書き換えることでinclude が呼びだされた時の挙動を変えことができます。

module M
  def self.append_features(base)
    p "#{base} include #{self}"
  end
end

class MyClass
  include M #=> "MyClass include M"
end

本記事とは直接関係ありませんが、引数のモジュールをインクルードするというinculde の機能を残したい場合はsuper を呼び出す必要があります。

  • superを呼び出さずに上書きする場合
module M
  def self.append_features(base)
   p base.ancestors
  end

  module ClassMethods
  end
end

class MyClass
  include M #=> [MyClass, Object, Kernel, BasicObject]
end
  • superを呼ぶ場合
module M
  def self.append_features(base)
    super
    p base.ancestors
  end

  module ClassMethods
  end
end

class MyClass
  include M #=> [MyClass, M, Object, Kernel, BasicObject]
end

処理のおさらい

以上より、ActiveRecord::Concernを使用することで以下のような処理が行われていることがわかります。

module M
  extend ActiveRecord::Concern

  module ClassMethods
    def c_method
      'c_method'
    end
  end
end

class MyClass
    # MyClass.extend M::ClassMethods
  include M
end

参考

メタプロRuby

Discussion