Techioz Blog

Ruby で undef を使用して定義を解除した後、インクルードされたクラスメソッドを再定義できなくなるのはなぜですか?

概要

Rails プロジェクトをアップグレードしていますが、以前のバージョンには Mocha が含まれていました。 any_instance メソッドを定義しました。私がアップグレードしている Rspec の新しいバージョンには、すべてのクラスに any_instance メソッドが含まれています。大量のテストを変更する必要がないように、すべてをアップグレードする間、しばらく Mocha を使い続ける必要があるため、any_instance の Mocha バージョンを使用したいと思います。

そのために、Rspec を使用して、最初に disable_monkey_patching! を使用して独自のモンキーパッチングを削除します。次に、Rspec の config.mock_with :mocha 呼び出しがあり、これにより mocha が Class の any_instance を定義します。すべてのクラスにモンキー パッチを適用するのは良くないことはわかっていますが、この質問は実際にはその理由を説明する良い教訓になります。しかし、私が見ている結果に興味があります。

上記は、私がこれを行う理由についての背景です。以下に示すのは、再現可能な最小限の例ですが、私には説明できないため、洞察をいただければ幸いです。

# Define a class
class A; end

# Define a module whose class methods I'd like to include in every class
module B
  module ClassMethods
    def a; end
  end
end

# Include method "a" in all classes
Class.send :include, B::ClassMethods

# Try it out!
A.a # <- works

ここで、undef を使用してこれを削除します。これが disable_monkey_patching の目的だからです。行います:

Class.class_exec { undef a }
A.a # <- undefined method `a' for A:Class (NoMethodError) -- that's expected 

ただし、今度は Class に別のメソッド “a” を定義する必要があります。これはモジュール C で定義します。

module C
  module ClassMethods
    def a; end
  end
end

Class.send :include, C::ClassMethods

私が混乱しているのは次の部分です。

A.a # <- undefined method `a' for A:Class (NoMethodError)

これにより、 undef は永久に定義を解除するように見えますが、最終的に使用できなくなるメソッドを定義しようとしても誰にも警告しません。なぜこのようなことが起こるのでしょうか?

MRI 3.2.2 および 2.7.2 で試した

解決策

include を呼び出しても、メソッドはレシーバーにコピーされません。これは、メソッド検索のために走査されるモジュールのリストに、含まれているモジュールを追加するだけです。

このリストは、A のシングルトン クラスの祖先を調べることで確認できます。

class A; end

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, Module, Object, Kernel, BasicObject]

B::ClassMethods をクラスに含めると、このリストはそれに応じて変更されます。

Class.include(B::ClassMethods)

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, B::ClassMethods, Module, Object, Kernel, BasicObject]
#           ^^^^^^^^^^^^^^^

B::ClassMethods が Class の後に追加されたことに注意してください。

ここで、Class をレシーバーとして undef / undef_method 経由でメソッドを「定義解除」すると、Class は (人為的な) NoMethodError を発生させてそのメソッドの呼び出しを阻止し、これにより以降のメソッド検索も終了します: (「人為的」と言ったのは、その方法はまだ残っています)

#<Class:A>  →  ...  →  Class  →  B::ClassMethods  →  ...
                         |             |
                       undef a         a
                                (never gets here)

別のモジュール C::ClassMethods を Class に含めると、リストの B::ClassMethods の前、ただし Class の後に追加されます。

Class.include(C::ClassMethods)

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, C::ClassMethods, B::ClassMethods, Module, Object, Kernel, BasicObject]
#           ^^^^^^^^^^^^^^^

そして、Class は依然として a の呼び出しを妨げているため、新しい a メソッドにも到達できません。

#<Class:A>  →  ...  →  Class  →  C::ClassMethods  →  B::ClassMethods  →  ...
                         |             |                   |
                       undef a         a                   a
                                (still not gets here)

実際の問題 (Mocha) については、まずオブジェクトの先祖を確認し、両方の any_instance メソッドが定義されている場所を特定する必要があります。

その後、 include / prepend を使用して、適切な場所にモンキーパッチを追加できます。