覆盖使用 #define_singleton_method 定义的实例方法并调用 original



我有一个类,可以在其实例上动态定义方法,如下所示

obj = Object.new
obj.define_singleton_method(:foo) { "foo" }

稍后,我想重新定义#foo,但能够调用原始实现。

使用普通类时,可以通过在预置方法中prepend MyModule和调用super来实现这一点。但#prepend在实例级别不可用。

我用#extend试过了,但似乎它根本没有覆盖#foo方法:

obj = Object.new
obj.define_singleton_method(:foo) { "foo" }
mod = Module.new
mod.define_method(:foo) { "module foo" }
obj.extend(mod)
obj.foo
# => "foo"

我研究了 RSpec gem,因为它们对#and_wrap_original实现了类似的行为(见 https://relishapp.com/rspec/rspec-mocks/v/3-8/docs/configuring-responses/wrapping-the-original-implementation(,但我不在测试环境中,而且看起来需要很多设置代码来实现这种行为(跟踪存根,在重置它们时有回调等(。

那么知道如何在纯 Ruby 中做到这一点吗?

看看obj.singleton_class.ancestors看看实际发生了什么。默认情况下,它是这样的

[#<Class:#<Object:0x00007febcf103160>>, Object, Kernel, BasicObject]

如果你做obj.extend(mod),你会得到这个:

[#<Class:#<Object:0x00007febcf103160>>, #<Module:0x00007febcd0bb088>, Object, Kernel, BasicObject]

所以顺序不好,因为模块"落后于"单例类。为了替换它,它需要在祖先链中处于较早的位置。您可以通过obj.singleton_class.prepend(mod).在这种情况下,祖先链是:

[#<Module:0x00007febcd0bb088>, #<Class:#<Object:0x00007febcf103160>>, Object, Kernel, BasicObject]

并且输出显示该方法已被覆盖:

obj.foo
# => "module foo"

最新更新