【问题标题】:How to temporarily redefine a method in Ruby?如何在 Ruby 中临时重新定义方法?
【发布时间】:2021-10-31 07:17:18
【问题描述】:

说,我有:

class Test
  def initialize(m)
    @m = m
  end

  def test
    @m
  end
end

如何暂时使Test的所有实例(现有的和新的)的方法#test返回113,然后再恢复原来的方法?

这听起来很简单,但我找不到实现它的好方法。可能是因为我对 Ruby 的了解太少。

到目前为止我发现的是:

# saving the original method
Test.send(:alias_method, :old_test, :test)

# redefining the method
Test.send(:define_method, :test) { 113 }

# restore the original method
Test.send(:alias_method, :test, :old_test)

这是什么工作,但据我了解,它还会重新定义现有的 #old_test(如果存在的话)?.. 感觉就像是一种 hack,而不是正确使用元编程?..

  1. 是否可以在不使用别名的情况下执行此类操作(但也可以不修改Test 的源代码)?
  2. 如果没有(或者如果有更简单/更好的方法),如果你可以修改Test 的源代码,你会怎么做?

如果您能描述实现同一目标的多种方法,即使是那些困难或不切实际的方法,我将不胜感激。只是想了解一下 Ruby 中元编程的灵活性和局限性:)

非常感谢????

附:我开始这一切的原因: 我正在使用 gem rack-throttle 来限制以 /api 开头的请求,但其他 url 不应该受到影响。我想测试所有这些以确保它有效。为了测试节流,我也必须将中间件添加到测试环境中。我已经成功地对其进行了测试(使用 minitest),但是不应限制所有其他测试 ApiController 的测试,因为如果我们需要在每个请求后等待 1 秒,它会使测试花费更长的时间。

我决定在 minitest 的 #setups 中用 { true } 修补 RequestSpecificIntervalThrottle#allowed? 以暂时禁用所有这些测试的节流,然后在 #teardowns 中重新启用它(否则测试节流的测试本身会失败)。如果你能告诉我你将如何处理这个问题,我将不胜感激。

但是现在我已经开始深入研究元编程,我也只是好奇如何实现这一点(暂时重新定义一个方法),即使我实际上并不打算使用它。

【问题讨论】:

  • 暂时重新定义方法的原因是什么?从表面上看(不知道原因),这似乎是异常处理或具有替代路径的条件语句更合适的情况。
  • @MichaelB 我现在已经编辑了问题——添加了原因:)
  • 存根可以满足您的需要吗?无需“重新定义”该方法,您可以在需要发生不同事情的测试中将其存根。
  • Rspec 支持allow_expect_any_instance_of(Class).to receive(method_name) relishapp.com/rspec/rspec-mocks/docs/working-with-legacy-code/…
  • @JohnP @melcher 顺便告诉你。我使用Test.any_instance.stubs(:test).returns(113) 完成了它,它运行得非常好,这基本上是你建议我的。非常感谢

标签: ruby metaprogramming class-eval instance-eval


【解决方案1】:

正如你在问题中所问的那样不切实际:)

class Test
  def initialize(m)
    @m = m
  end

  def test
    @m
  end
end

t = Test.new(9)
t.test # => 9

Test.define_method(:test) { 113 }
t.test # => 113

Test.define_method(:test) { instance_variable_get(:@m) }
t.test # => 9

Test.undef_method(:test)
t.test # will raise NoMethodError

【讨论】:

    【解决方案2】:

    您可以使用instance_method 从任何实例方法中获取UnboundMethod 对象:

    class Foo
      def bar
        "Hello"
      end
    end
    old_method = Foo.instance_method(:bar)
    # Redifine the method
    Foo.define_method(:bar) do
      puts "Goodbye"
    end
    puts Foo.new.bar # Goodbye
    
    # restore the old method:
    Foo.define_method(old_method.name, old_method)
    

    未绑定方法是对该方法在对象化时的引用:对基础类的后续更改不会影响未绑定方法。

    类方法的等价物是:

    class Foo
      def self.baz
        "Hello"
      end
    end
    
    old_method = Foo.method(:baz).unbind
    

    如果你想让世界上最小的(也许是最没用的)存根库,你可以这样做:

    class Stubby
      def initialize(klass, method_name, &block)
        @klass = klass
        @old_method = klass.instance_method(method_name)
        @klass.define_method(method_name, &block)
      end
    
      def restore
        @klass.define_method(@old_method.name, @old_method)
      end
    
      def run_and_restore
        yield
      ensure
        restore
      end
    end
    
    puts Foo.new.bar # Hello
    
    Stubby.new(Foo, :bar) do
      "Goodbye"
    end.run_and_restore do
      puts Foo.new.bar # Goodbye
    end
    
    puts Foo.new.bar # Hello
    

    【讨论】:

    • 非常感谢 Max 的解释!似乎我的问题是我在做Foo.send(:define_method, old_method.name, &old_method)(额外的&符号)并且它抱怨TypeError: wrong argument type UnboundMethod (expected Proc)?‍♂️。非常感谢您的回答并帮助我找到我的愚蠢错误!顺便说一句,您可能还应该将puts Foo.bar 更改为Foo.new.bar,因为我想这就是您打算写的,因为我们需要实例方法,而不是类方法:) 再次感谢! :)
    • 糟糕。你是对的 - 我打算写Foo.new.bar
    猜你喜欢
    • 2011-05-26
    • 2014-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-06
    • 1970-01-01
    相关资源
    最近更新 更多