EDIT:距离我最初写这个答案已经过去了 9 年,值得做一些整容手术以使其保持最新状态。
可以看到修改前的最新版本here.
您不能调用被覆盖按名称或关键字的方法。这是应该避免猴子修补并首选继承的众多原因之一,因为显然你can打电话给被覆盖 method.
避免猴子修补
遗产
因此,如果可能的话,您应该更喜欢这样的东西:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
如果您控制创建Foo
对象。只需更改创建一个的每个地方Foo
而是创建一个ExtendedFoo
。如果您使用以下命令,效果会更好依赖注入设计模式, the 工厂方法设计模式, the 抽象工厂设计模式或类似的东西,因为在这种情况下,只有一个地方需要改变。
代表团
If you do not控制创建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
对象进入您的代码,您将其包装到另一个对象中,然后使用that对象而不是代码中其他地方的原始对象。
这使用了Object#DelegateClass辅助方法来自delegatestdlib 中的库。
“干净”的猴子补丁
Module#prepend: 混合前置
上述两种方法都需要更改系统以避免猴子补丁。本节展示了猴子修补的首选且侵入性最小的方法,如果无法更改系统的话。
Module#prepend被添加来或多或少地支持这个用例。Module#prepend
做同样的事情Module#include
,除了它直接混合在 mixin 中below班上:
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 上不起作用)这样的事情,即include
使用 mixin 代替prepend
ing it:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
不幸的是,这行不通。这是一个好主意,因为它使用继承,这意味着您可以使用super
。然而,Module#include插入 mixinabove继承层次结构中的类,这意味着FooExtensions#bar
永远不会被调用(如果它were叫做super
实际上不会指Foo#bar
而是为了Object#bar
不存在),因为Foo#bar
总是会先被找到。
方法包装
最大的问题是:我们如何才能坚持下去bar
方法,而不实际保留实际方法?正如经常出现的那样,答案就在于函数式编程。我们掌握了该方法作为实际的方法object,并且我们使用闭包(即块)来确保我们并且只有我们抓住那个物体:
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
只是一个局部变量,它会在类体末尾超出范围,并且无法从任何地方访问它,even使用反射!自从Module#define_method占用一个块,并且块靠近其周围的词汇环境(即why我们正在使用define_method
代替def
here), it (and only它)仍然可以访问old_bar
,即使它超出了范围。
简短说明:
old_bar = instance_method(:bar)
在这里我们正在包装bar
方法转化为UnboundMethod方法对象并将其分配给局部变量old_bar
。这意味着,我们现在有办法坚持下去bar
即使它已被覆盖。
old_bar.bind(self)
这有点棘手。基本上,在 Ruby(以及几乎所有基于单分派的 OO 语言)中,方法绑定到特定的接收者对象,称为self
在红宝石中。换句话说:一个方法总是知道它被调用的对象是什么,它知道它的对象是什么self
是。但是,我们直接从类中获取方法,它怎么知道它是什么self
is?
嗯,事实并非如此,这就是为什么我们需要bind our UnboundMethod
首先到一个对象,这将返回一个Method我们可以调用的对象。 (UnboundMethod
s 无法被调用,因为他们不知道在不知道自己的情况下该怎么做self
.)
而我们该怎么办bind
它到?我们简单地bind
它对我们自己来说,它就会以这种方式表现exactly像原来的一样bar
将有!
最后,我们需要调用Method
这是从返回的bind
。在 Ruby 1.9 中,有一些漂亮的新语法(.()
),但如果你使用的是 1.8,你可以简单地使用call方法;就是这样.()
无论如何都会被翻译成。
以下是其他几个问题,其中解释了其中一些概念:
- 如何在 Ruby 中引用函数?
- Ruby 的代码块与 C♯ 的 lambda 表达式相同吗?
“肮脏的”猴子补丁
alias_method chain
我们在猴子修补中遇到的问题是,当我们覆盖该方法时,该方法就消失了,因此我们无法再调用它。所以,让我们制作一个备份副本!
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 将猴子补丁限制在特定范围内。我将在这里使用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
method.
然而,尚不清楚您是否以及如何访问bar
的返回值在bar:after
。也许我们可以(滥用)使用super
关键词?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
替代品
之前的组合器相当于prepend
使用重写方法调用 mixinsuper
在非常end该方法的。同样,后组合器相当于prepend
使用重写方法调用 mixinsuper
在非常开始该方法的。
你也可以先做一些事and打电话后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
and
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
keyword
这个想法添加了一个新的关键字,类似于super
,这允许您调用被覆盖方法同样的方法super
让你打电话被覆盖 method:
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
这样做的主要问题是它向后不兼容:如果你有调用的方法old
,您将无法再调用它!
替代品
super
在重写方法中prepend
ed mixin 本质上与old
在此提案中。
redef
keyword
与上面类似,但不是添加新关键字calling覆盖的方法并离开def
仅此而已,我们添加一个新关键字重新定义方法。这是向后兼容的,因为目前的语法无论如何都是非法的:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
而不是添加two新的关键词,我们也可以重新定义它的含义super
inside redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
替代品
redef
调用一个方法相当于重写一个方法prepend
编辑混合。super
在重写方法中的行为类似于super
or old
在此提案中。