当猴子修补实例方法,你可以调用覆盖的方法从新的实现?

假设我在一个类中修补一个方法,我如何从重写方法调用重写方法?例如,有点像super

如。

class Foo
def bar()
"Hello"
end
end


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


>> Foo.new.bar == "Hello World"
96821 次浏览

看看混叠方法,这是将方法重命名为一个新名称。

要了解更多信息和起点,请查看替换方法(特别是第一部分)。 Ruby API文档,也提供了(一个不太详细的)例子

编辑:从我最初写这个答案到现在已经9年了,它值得一些整容手术来保持它的最新。

你可以看到编辑在这里之前的最后一个版本。


不能通过名称或关键字调用覆盖方法。这是为什么应该避免monkey patch而选择继承的众多原因之一,因为显然你可以调用了覆盖方法。

避免猴子补丁

继承

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

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 helper方法。

“清洁”猴子修补

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模块的前置和派生

Mixin继承(损坏)

我见过一些人尝试(并询问为什么它不能在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永远不会被调用(如果它被调用,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),因此(和只有 it)仍然可以访问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对象,然后我们可以调用。(__abc1不能被调用,因为它们在不知道self的情况下不知道该做什么。)

我们bind它到什么?我们简单地将它bind留给自己,这样它就会像原来的bar那样表现完全 !

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

下面是其他几个问题,其中一些概念是解释的:

“肮脏的”猴子修补

alias_method

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

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将monkey补丁限制在特定的范围内。我将在这里使用上面的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的例子:如何启用猴子补丁的具体方法?


放弃的想法

在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方法的执行。

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

class Foo
def bar
'Hello'
end
end


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

更换

前面的组合子相当于用一个覆盖方法prepending一个mixin,该方法在方法的结束处调用super。同样,after组合子等价于用覆盖方法prepending一个mixin,该方法在方法的开始处调用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让你调用覆盖方法一样:

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'

更换

__abc0ing方法等价于覆盖prepended mixin中的方法。覆盖方法中的super行为类似于本建议中的superold

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