如何为 Timecop 修补 File 和 CoreExtensions

How to patch File and CoreExtensions for Timecop

我需要猴子补丁文件。 Timecop 不影响文件系统报告的时间,这是 File.atime 使用的时间,而这又是 HttpClient 在将文件发布到服务器时使用的时间,这反过来意味着 VCR 不能完全按预期工作。 AFAIK,这意味着我不能使用优化。

我不明白这是怎么回事:

class File
  def atime
    "this one happens"
  end
end

module CoreExtensions
  module File
    module TimecopCompat
      def atime
        "this one does not"
      end
    end
  end
end

File.include CoreExtensions::File::TimecopCompat

File.new('somefile').atime # --> "this one happens"

为什么基于模块的猴子补丁没有发生?我需要更改什么才能使其正常工作?我应该使用其他类型的猴子补丁吗?

问题与 include 将模块附加到祖先链的方式有关。 “Ruby modules: Include vs Prepend vs Extend”非常详细地概述了 includeprepend 之间的差异。

看看这两个例子:

class Foo
  def hello
    "1"
  end
end

module Bar
  def hello
    "2"
  end
end

Foo.include Bar

Foo.new.hello
# => "1"
Foo.ancestors
# => [Foo, Bar, Object, Kernel, BasicObject]

对比

class Foo
  def hello
    "1"
  end
end

module Bar
  def hello
    "2"
  end
end

Foo.prepend Bar

Foo.new.hello
# => "2"
Foo.ancestors
# => [Bar, Foo, Object, Kernel, BasicObject]

基本上,您希望在您的情况下使用 prepend,因为 include 不会覆盖现有方法。

include 不是什么神奇的东西。其实很简单:它使模块成为它所混入的 class 的 superclass 。现在:superclass 方法会覆盖 subclass 方法吗?不,当然不是,恰恰相反。

因此,include 不可能覆盖 class 模块 included 进入的方法。

这就是 prepend 的用途,它混合在祖先层次结构的 开头 的模块中。 (不幸的是,这不能简单地用继承来解释,它是不同的东西。)

让我们在不更改问题的情况下简化您的示例。

module TimecopCompat
  def atime
    "this one does not"
  end
end

我单独留下了 class File 因为它已经有一个实例方法 File#atime.

File.new('temp').atime
  #=> 2019-07-16 20:20:51 -0700

正如其他答案所解释的那样,执行

File.include TimecopCompat

结果:

File.ancestors
  #=> [File, TimecopCompat, IO, File::Constants, Enumerable, Object, Kernel, BasicObject] 
File.new('temp').atime
  #=> 2019-07-16 20:20:51 -0700

正在执行

File.prepend TimecopCompat

结果:

File.ancestors
  #=> [TimecopCompat, File, IO, File::Constants, Enumerable, Object, Kernel, BasicObject] 
File.new('temp').atime
  #=> "this one does not" 

但是,更改任何核心方法的行为是不好的做法,因为它的原始行为可能依赖于程序中的其他地方。

这里有两种可接受的做法。第一个是创建一个方法(new_atime,比方说),它有一个 File 对象(file,比方说)作为它的参数:

file = File.new('temp')
x = new_atime(file)

new_atime 不能与作为其接收者的 File 对象链接,但对于安全和稳健的解决方案来说,这是一个很小的代价。

第二种选择是使用Refinementsrefine File class.

module RefinedFile
  refine File do
    def atime
      "this one does not"
    end
  end
end

class C
  using RefinedFile
  File.new('temp').atime
end
  #=> "this one does not"

我们可以确认 File#atime 在 class 之外没有被改动 C:

File.new('temp').atime
  #=> 2019-07-16 20:20:51 -0700