kakudooo docs

inheritedメソッド

inheritedメソッドとは?

Classクラスに実装されているメソッド。

クラスのサブクラスが定義された時、新しく生成されたサブクラスを引数にインタプリタから呼び出されます。このメソッドが呼ばれるタイミングはクラス定義文の実行直前です。

とあるように、あるクラスのサブクラスが定義された時に必ず呼び出される関数を定義することができる。

class Foo
  def Foo.inherited(subclass)
    puts "class \"#{self}\" was inherited by \"#{subclass}\""
  end
end
class Bar < Foo
  puts "executing class body"
end

# => class "Foo" was inherited by "Bar"
#    executing class body

Active Recordとinheritedメソッド

Active Recordではinheritedメソッドが活用されている。

例えば、ActiveRecord::Relationクラスの動的生成においてである。ActiveRecord::Baseを継承したModelクラスを定義すると、作成したModelクラスの名前空間ごとにActiveRecord_Relationクラスが定義される。

以下のような内部クラスが自動的に定義される。

User::ActiveRecord_Relation

なぜModelごとにActiveRecord::Relationクラスが必要になるかについては、以下の資料がとても詳細かつ分かりやすい(→ クラスメソッドやScopeを呼び出すため。)

inheritedメソッドはこの、ActiveRecord_Relationクラスの動的生成に一役買っている。

eager_loadはActiveRecord::Relationクラスを返す

例として、Userモデルからeager_loadを呼び出したとする。 以下はいずれもActiveRecord::Relationクラスで定義されている。(厳密にはmixinされるモジュール)

def eager_load(*args)
  check_if_method_has_arguments!(__callee__, args)
  spawn.eager_load!(*args)
end

def eager_load!(*args) # :nodoc:
  self.eager_load_values |= args
  self
end

https://github.com/rails/rails/blob/3235827585d87661942c91bc81f64f56d710f0b2/activerecord/lib/active_record/relation/query_methods.rb#L290

ActiveRecord::BaseとDelegateCacheモジュールをmixinしている

ActiveRecord::BaseDelegation::DelegateCacheがmixinされる

module ActiveRecord
  class Base
    ...
    extend Delegation::DelegateCache
    ...
  end
end

https://github.com/rails/rails/blob/3954e4681de1013d1dc2efc166fb765be62650bd/activerecord/lib/active_record/base.rb#L296

定義したModelの名前空間内に、ActiveRecord_Relationが定義される

ActiveRecord::BaseとDelegateCacheモジュールのinheritedメソッドにより、ActiveRecord::Baseのサブクラス(今回の例だとUserクラス)を定義したタイミングで、User::ActiveRecord_Relationクラスが作成される。

User.eager_load(:profile)とすると、User::ActiveRecord_Relationクラスに定義されているeager_loadが呼び出されることになる。

module DelegateCache
  ...
  def initialize_relation_delegate_cache
    @relation_delegate_cache = cache = {}
    Delegation.delegated_classes.each do |klass|
      delegate = Class.new(klass) {
        include ClassSpecificRelation
      }
      include_relation_methods(delegate)
      mangled_name = klass.name.gsub("::", "_")
      const_set mangled_name, delegate
      private_constant mangled_name

      cache[klass] = delegate
    end
  end

  def inherited(child_class)
    child_class.initialize_relation_delegate_cache
    super
  end
  ...
end

知っているとどんな時に便利か?

例えば、以下のようなメソッドについて、RSpecでテストを作成しているとする。

def hoge
  User.eager_load(:profile).find_each do |user|
    ...
    user.save!
  rescue => e
    Rails.logger.warn(e)
  end
end

ブロック内で例外が発生した場合をテストするために、user.save!をスタブして例外を返すようにしたい。 動的に定義されるクラスをinstance_doubleでモックするために、eager_loadをはじめとしたメソッドチェーンをすべてスタブしていくことにする。(テストの仕方がまずいというのは一旦おいておく) その上で、eager_loadメソッドの返すオブジェクトのクラスが

User.eager_load(:profile).class
=> User::ActiveRecord_Relation

であることがわかった上で

をActive Recordのソースコードから読み取ることができる。

User::ActiveRecord_Relationはinheritedメソッドによってサブクラスの定義時に作成され、元はActiveRecord::Relationであることから、以下のようにモックすることができる。

describe 'User' do
  describe 'class method hoge' do
    context 'when raised error in find_each block' do
      let(:user) { create(:user) }
      before do
        relation = instance_double(ActiveRecord::Relation)
        allow(User).to receive(:eager_load).and_return(relation)
        allow(user).to receive(:save!).and_raise('error!!')
        allow(relation).to receive(:find_each).and_yield(user)
      end

      it 'logged error message' do
        expect(Rails.logger).to receive(:warn).with("error!!")
        User.hoge
      end
    end
  end
end

参考