当对实例方法进行猴子补丁时,是否可以从新的实现中调用被覆盖的方法?

498

假设我正在类中进行Monkey Patching,我如何从覆盖的方法中调用被覆盖的方法?即类似于super的东西。

例如:

class Foo
  def bar()
    "Hello"
  end
end 

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

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

第一个Foo类不应该是其他类吗?第二个Foo类是它的继承者吗? - Draco Ater
1
不,我正在进行猴子补丁。我希望有类似于super()的东西可以用来调用原始方法。 - James Hollingworth
2
当你无法控制Foo的创建和Foo::bar的使用时,就需要这样做。因此,您必须对该方法进行猴子补丁 - Halil Özgür
3个回答

1266

编辑:自我写下这个答案已经9年了,现在需要一些整容来保持时效性。

你可以在这里查看编辑前的版本。


你不能通过名称或关键字调用被覆盖的方法。这就是为什么应该避免猴子补丁并优先使用继承的许多原因之一,因为显然你可以调用被重写的方法。
避免猴子补丁
继承
因此,如果可能的话,你应该更喜欢像这样的东西:
class Foo
  def bar
    'Hello'
  end
end 

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

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

如果您控制Foo对象的创建,这将起作用。只需更改每个创建Foo的地方,以创建一个ExtendedFoo。如果您使用依赖注入设计模式工厂方法设计模式抽象工厂设计模式或类似的东西,那么这将更加有效,因为在这种情况下,您只需要更改一个地方。

委托

如果您不能控制Foo对象的创建,例如因为它们是由框架创建的,而该框架位于您无法控制的范围之外(例如),那么您可以使用包装器设计模式

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 Prepending
以上两种方法需要更改系统以避免猴子补丁。如果更改系统不是一种选择,则本节展示了最优先和最不具侵入性的猴子补丁方法。

Module#prepend被添加以支持更多或更少的这种用例。 Module#prependModule#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模块prepend与派生

混入继承(已损坏)

我看到有些人尝试(并在StackOverflow上问为什么它不起作用),例如像这样include一个混入,而不是prepend它:

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永远不会被调用(如果它被调用,super实际上不会指向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(以及几乎所有基于单分派的面向对象语言中),方法绑定到特定的接收器对象,称为 Ruby 中的 self。换句话说:方法总是知道它被调用的对象,它知道它的 self 是什么。但是,我们直接从类中获取了方法,它怎么知道它的 self 是什么呢?
嗯,它不知道,这就是为什么我们需要首先将我们的 UnboundMethod bind 到一个对象上,这将返回一个 Method 对象,然后我们可以调用它。(UnboundMethod 无法被调用,因为它们不知道如何在不知道自己的 self 的情况下执行。)
那么我们应该将其绑定到什么?我们只需将其绑定到自己,这样它就会像原始的 bar 一样表现!
最后,我们需要调用从bind返回的Method。在Ruby 1.9中,有一些新的巧妙语法可以做到这一点(.()),但如果您使用的是1.8版本,则可以简单地使用call方法;无论如何,.()都会被翻译成它。

以下是其他一些问题,其中解释了其中一些概念:

“肮脏”的Monkey Patching

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!

您可以在这个问题中看到一个更复杂的使用细化的例子:如何为特定方法启用猴子补丁?


被废弃的想法

在 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:after中访问bar的返回值。也许我们可以(滥用)super关键字?
class Foo
  def bar
    'Hello'
  end
end 

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

替换

before组合器相当于在一个覆盖方法的mixin中使用prepend,并在方法的最后调用super。同样地,after组合器相当于在一个覆盖方法的mixin中使用prepend,并在方法的开头调用super

您还可以在调用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 的新关键字,它允许您以与 super 相同的方式调用 被覆盖 方法,就像 super 允许您调用 被覆盖 方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

这样做的主要问题是不向后兼容: 如果你有一个叫做old的方法,你将无法再调用它!
替代方案
在一个prepend混入中重写方法中的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方法等同于在prepend混入中覆盖该方法。在覆盖方法中,super的行为类似于此提案中的superold


1
@KandadaBoggu:我正在努力弄清楚您的确切意思 :-) 但是,我非常确定它不比Ruby中的任何其他元编程更不安全。特别是,每次调用UnboundMethod#bind都会返回一个新的、不同的Method,因此,无论您是连续两次调用还是从不同的线程同时调用两次,我都没有看到任何冲突产生。 - Jörg W Mittag
2
自从我开始学习Ruby和Rails以来,一直在寻找像这样的有关修补程序的解释。非常好的答案!唯一遗漏的是有关class_eval与重新打开类之间区别的说明。在这里:https://dev59.com/mGox5IYBdhLWcg3wJxH4#10304721 - Евген
1
Ruby 2.0有改进功能 http://blog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk/ - NARKOZ
顺便说一下,可以通过库的方式完全实现redef(稍作语法修改)。 - Ajedi32
5
你在哪里找到 oldredef?我的 2.0.0 没有它们。啊,很难不去想那些未能进入 Ruby 的 其他竞争性想法包括: - Nakilon
显示剩余8条评论

12

请看别名方法,这是将方法重命名为新名称的一种方法。

有关更多信息和起点,请参阅此替换方法文章(特别是第一部分)。 也可以参考Ruby API文档提供的(不太详细的)示例。


-3
重写的类必须在包含原始方法的类之后重新加载,因此在将进行重写的文件中使用require

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接