【问题标题】:When monkey patching an instance method, can you call the overridden method from the new implementation?当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗?
【发布时间】:2011-05-27 02:05:59
【问题描述】:

假设我正在修补类中的方法,我怎么能从覆盖方法中调用被覆盖的方法? IE。有点像super

例如

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"

【问题讨论】:

  • 第一个 Foo 类不应该是其他类,而第二个 Foo 不应该继承自它吗?
  • 不,我在打猴子补丁。我希望有类似 super() 的东西可以用来调用原始方法
  • 当你不控制Foo的创建和Foo::bar的使用时需要这个。所以你必须monkey patch这个方法。

标签: ruby monkeypatching


【解决方案1】:

编辑:我最初写这个答案已有 9 年了,它应该进行一些整容手术以保持最新状态。

可以看到编辑前的最后一个版本here。


您不能通过名称或关键字调用 overwritten 方法。这就是为什么应该避免猴子修补而优先使用继承的众多原因之一,因为显然您可以调用 overridden 方法。

避免猴子修补

继承

所以,如果可能的话,你应该更喜欢这样的东西:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'

如果您控制Foo 对象的创建,则此方法有效。只需将创建Foo 的每个位置更改为创建ExtendedFoo。如果你使用Dependency Injection Design Pattern、Factory Method Design Pattern、 Abstract Factory Design Pattern 或类似的东西,这会更好,因为在这种情况下,只有你需要改变的地方。

代表团

如果您不控制Foo 对象的创建,例如因为它们是由您无法控制的框架创建的(例如),那么您可以使用Wrapper Design Pattern:

require 'delegate'

class Foo
  def bar
    'Hello'
  end
end 

class WrappedFoo < DelegateClass(Foo)
  def initialize(wrapped_foo)
    super
  end

  def bar
    super + ' World'
  end
end

foo = Foo.new # this is not actually in your code, it comes from somewhere else

wrapped_foo = WrappedFoo.new(foo) # this is under your control

wrapped_foo.bar # => 'Hello World'

基本上,在系统的边界,Foo 对象进入您的代码,您将其包装到另一个对象中,然后在其他任何地方使用 那个 对象而不是原始对象你的代码。

这使用 stdlib 中 delegate 库中的 Object#DelegateClass 辅助方法。

“清洁”猴子补丁

Module#prepend: Mixin 前置

上述两种方法都需要更改系统以避免猴子补丁。本节展示了猴子修补的首选且侵入性最小的方法,如果不能更改系统。

Module#prepend 被添加以或多或少地支持此用例。 Module#prepend 与 Module#include 做同样的事情,只是它直接在类中下面混入了 mixin:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  prepend FooExtensions
end

Foo.new.bar # => 'Hello World'

注意:我在这个问题中也写了一点Module#prepend:Ruby module prepend vs derivation

混合继承(损坏)

我看到有些人尝试(并询问为什么它在 StackOverflow 上不起作用)这样的事情,即 includeing 一个 mixin 而不是 prepending 它:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

很遗憾,这行不通。这是个好主意,因为它使用继承,这意味着你可以使用super。然而,Module#include 在继承层次结构的类上方插入了mixin,这意味着FooExtensions#bar永远不会被调用(如果它被调用,@987654367 @ 实际上不会引用Foo#bar,而是引用不存在的Object#bar),因为总是会首先找到Foo#bar。

方法包装

最大的问题是:我们如何才能坚持bar 方法,而不实际保留实际方法?答案就在函数式编程中,就像它经常做的那样。我们将方法作为一个实际的对象来持有,并且我们使用一个闭包(即一个块)来确保我们并且只有我们持有该对象:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  old_bar = instance_method(:bar)

  define_method(:bar) do
    old_bar.bind(self).() + ' World'
  end
end

Foo.new.bar # => 'Hello World'

这很干净:由于old_bar只是一个局部变量,它会在类体的末尾超出范围,并且无法从任何地方访问它,甚至使用反射!而且由于Module#define_method 占用了一个块,并且块关闭了它们周围的词法环境(这为什么我们在这里使用define_method 而不是def),它 (并且只有它)仍然可以访问old_bar,即使它已经超出范围。

简短说明:

old_bar = instance_method(:bar)

这里我们将bar 方法包装到UnboundMethod 方法对象中,并将其分配给局部变量old_bar。这意味着,即使 bar 被覆盖,我们现在也有办法保留它。

old_bar.bind(self)

这有点棘手。基本上,在 Ruby(以及几乎所有基于单调度的 OO 语言中)中,方法绑定到特定的接收器对象,在 Ruby 中称为 self。换句话说:一个方法总是知道它被调用了什么对象,它知道它的self 是什么。但是,我们直接从一个类中抓取方法,它怎么知道它的self是什么?

好吧,它没有,这就是为什么我们需要先将 bind 和 UnboundMethod 指向一个对象,这将返回一个 Method 对象,然后我们可以调用它。 (UnboundMethods 不能被调用,因为他们不知道自己的self 不知道该怎么做。)

我们bind 它是做什么用的?我们只是给自己bind 它,这样它就会像原来的bar 那样表现完全!

最后,我们需要调用从bind 返回的Method。在 Ruby 1.9 中,有一些漂亮的新语法 (.()),但如果您使用的是 1.8,则可以简单地使用 call 方法;无论如何,这就是 .() 被翻译成的内容。

这里有几个其他问题,其中解释了其中一些概念:

“肮脏的”猴子补丁

alias_method链

我们在猴子补丁中遇到的问题是,当我们覆盖该方法时,该方法就消失了,所以我们不能再调用它了。所以,让我们做个备份吧!

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  alias_method :old_bar, :bar

  def bar
    old_bar + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Foo.new.old_bar # => 'Hello'

问题在于我们现在已经用一个多余的old_bar 方法污染了命名空间。这个方法会出现在我们的文档中,它会出现在我们 IDE 的代码完成中,它会出现在反射过程中。此外,它仍然可以调用,但大概是我们猴子修补了它,因为我们一开始不喜欢它的行为,所以我们可能不希望其他人调用它。

尽管它有一些不受欢迎的特性,但不幸的是,它已通过 AciveSupport 的Module#alias_method_chain 普及。

旁白:Refinements

如果您只需要在几个特定位置而不是整个系统中的不同行为,您可以使用 Refinements 将猴子补丁限制在特定范围内。我将在这里使用上面的Module#prepend 示例进行演示:

class Foo
  def bar
    'Hello'
  end
end 

module ExtendedFoo
  module FooExtensions
    def bar
      super + ' World'
    end
  end

  refine Foo do
    prepend FooExtensions
  end
end

Foo.new.bar # => 'Hello'
# We haven’t activated our Refinement yet!

using ExtendedFoo
# Activate our Refinement

Foo.new.bar # => 'Hello World'
# There it is!

你可以在这个问题中看到一个更复杂的使用 Refinements 的例子:How to enable monkey patch for specific method?


放弃的想法

在 Ruby 社区确定 Module#prepend 之前,有多种不同的想法在流传,您可能偶尔会在较早的讨论中看到这些想法。所有这些都包含在Module#prepend 中。

方法组合器

一个想法是来自 CLOS 的方法组合器的想法。这基本上是面向切面编程子集的一个非常轻量级的版本。

使用类似的语法

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end

  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

您将能够“挂钩”bar 方法的执行。

然而,尚不清楚您是否以及如何访问bar 在bar:after 中的返回值。也许我们可以(ab)使用super 关键字?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

替换

before 组合符等价于prepend 使用覆盖方法的mixin,该方法在方法的最end 处调用super。同样,after 组合符等价于 prepend 使用一个在方法的最开头调用 super 的覆盖方法的 mixin。

你也可以在调用super之后和,你可以多次调用super,同时检索和操作super的返回值,使得prepend更多比方法组合器更强大。

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end
end

# is the same as

module BarBefore
  def bar
    # will always run before bar, when bar is called
    super
  end
end

class Foo
  prepend BarBefore
end

和

class Foo
  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

# is the same as

class BarAfter
  def bar
    original_return_value = super
    # will always run after bar, when bar is called
    # has access to and can change bar’s return value
  end
end

class Foo
  prepend BarAfter
end

old关键字

这个想法增加了一个类似于super的新关键字,它允许你调用overwritten方法,就像super让你调用overridden方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

这样做的主要问题是它向后不兼容:如果您有名为old 的方法,您将无法再调用它!

替换

prepended mixin 中的覆盖方法中的super 与本提案中的old 基本相同。

redef关键字

与上面类似,但我们不是为调用被覆盖的方法添加一个新关键字并单独留下def,而是为重新定义方法添加一个新关键字。这是向后兼容的,因为目前的语法无论如何都是非法的:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

除了添加两个新关键字,我们还可以在redef中重新定义super的含义:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

替换

redefining 方法等同于在 prepended mixin 中覆盖该方法。覆盖方法中的super 的行为类似于本提案中的super 或old。

【讨论】:

  • @Jörg W Mittag,方法包装方法线程安全吗?当两个并发线程在同一个old_method 变量上调用bind 时会发生什么?
  • @KandadaBoggu:我试图弄清楚你的意思是什么确切地 :-) 但是,我很确定它的线程安全性不亚于任何Ruby 中的其他类型的元编程。特别是,每次对UnboundMethod#bind 的调用都会返回一个新的、不同的Method,因此,无论您是连续调用两次还是从不同线程同时调用两次,我都没有看到任何冲突。
  • 自从我开始使用 ruby​​ 和 rails 以来,我一直在寻找这样的补丁解释。很好的答案!我唯一缺少的是关于 class_eval 与重新开课的注释。这是:stackoverflow.com/a/10304721/188462
  • 在哪里可以找到old 和redef?我的 2.0.0 没有它们。啊,很难不错过 没有进入 Ruby 的其他竞争想法是:
【解决方案2】:

看看别名方法,这是将方法重命名为新名称。

有关更多信息和起点,请查看replacing methods article(尤其是第一部分)。 Ruby API docs 还提供了(一个不太详细的)示例。

【讨论】:

    【解决方案3】:

    将进行覆盖的类必须在包含原始方法的类之后重新加载,因此require它在将进行覆盖的文件中。

    【讨论】:

      猜你喜欢
      • 2015-03-23
      • 1970-01-01
      • 2012-07-26
      • 2015-11-10
      • 1970-01-01
      • 1970-01-01
      • 2010-10-01
      相关资源
      最近更新 更多