【问题标题】:How to patch File and CoreExtensions for Timecop如何为 Timecop 修补文件和核心扩展
【发布时间】:2019-07-26 19:10:04
【问题描述】:

我需要猴子补丁文件。 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"

为什么不进行基于模块的猴子补丁?我需要改变什么才能让它工作?我应该使用另一种猴子补丁吗?

【问题讨论】:

  • 与您的问题不严格相关,但请注意使用 Timecop 是 no longer recommended。现在人们普遍认为最好使用 Rails TimeHelpers,特别是因为 Timecop 已经一年多没有更新了。在撰写本文时,它还没有正式支持最新的 ruby​​。 (至少它没有在 repo 的 travis.yml 中测试)当然,如果您正在维护一个没有时间升级的旧应用程序,那么这是继续使用它的充分理由。请注意。

标签: ruby monkeypatching


【解决方案1】:

问题与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 不会覆盖现有方法。

【讨论】:

  • 啊,整洁,我明白了:File.include 意味着如果该函数尚未在 File 中定义,则会在包含的模块中查找它。 Prepend 意味着如果该函数未在前置模块中定义,则在 File 中查找它(依此类推)。整洁!
【解决方案2】:

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

因此,include 不可能覆盖模块正被 included 加入的类的方法。

这就是prepend 的用途,它在祖先层次结构的开始 处混入一个模块。 (不幸的是,这不能简单地用继承来解释,它是不同的。)

【讨论】:

    【解决方案3】:

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

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

    我已经单独留下了 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 类。

    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#atimeC类之外没有被修改:

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

    【讨论】:

    • 感谢您的回答,但对file.atime 的实际调用在httpclient gem 中,而这又被API gem(在本例中为Boxr)包装了录像机。 AFAIK,这些方法都不适用于这种情况。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-10
    • 1970-01-01
    • 2020-10-27
    • 2015-07-06
    • 1970-01-01
    • 2023-03-12
    相关资源
    最近更新 更多