当猴子修补实例方法时,您可以从新实现中调用重写的方法吗?
假设我正在修补类中的方法,我如何从重写方法中调用重写方法?即有点像 super
例如
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
Say I am monkey patching a method in a class, how could I call the overridden method from the overriding method? I.e. Something a bit like super
E.g.
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
编辑:自从我最初写这个答案以来已经过去了 9 年,它值得进行一些整容手术以使其保持最新状态。
您可以在此处查看编辑前的最新版本。
您不能通过名称或关键字覆盖方法来调用。这是应避免猴子修补并首选继承的众多原因之一,因为显然您可以调用重写方法。
避免 Monkey 修补
继承
因此,如果可能的话,您应该更喜欢这样的东西:
如果您控制
Foo
对象的创建,那么这种方法就有效。只需更改创建Foo
的每个位置即可创建ExtendedFoo
。如果您使用依赖注入设计模式,即工厂方法设计模式, 抽象工厂设计模式 或类似的东西,因为在这种情况下,只有一个地方需要更改。委托
如果您不控制
Foo
对象的创建,例如因为它们是由您无法控制的框架创建的(例如ruby-on-rails 例如),然后您可以使用包装设计模式:基本上,在系统的边界,
Foo
对象进入您的代码,您将其包装到另一个对象中,然后在代码中的其他地方使用该对象而不是原始对象。这使用
Object#DelegateClass
来自
delegate
stdlib 中的库。“干净”的猴子补丁
Module#prepend
: Mixin Prepending上述两种方法都需要更改系统以避免猴子修补。本节展示了猴子修补的首选且侵入性最小的方法,如果无法更改系统的话。
Module#prepend
已添加到或多或少地支持这个用例。Module#prepend
与Module#include
做同样的事情,除了它直接在类下面的 mixin 中混合:注意:我还写了一个关于此问题中的
Module#prepend
的一些信息:Ruby 模块前置与派生Mixin 继承(破碎)
我看到有些人尝试(并询问为什么它在 StackOverflow 上不起作用)这样的事情,即
include
混合而不是prepend
ing it:不幸的是,这行不通。这是一个好主意,因为它使用继承,这意味着您可以使用
super
。但是,Module#include
插入继承层次结构中类之上的 mixin,这意味着FooExtensions#bar
永远不会被调用(如果被调用,< code>super 实际上不会引用Foo#bar
,而是引用不存在的Object#bar
),因为Foo#bar< /code> 总是会首先被找到。
方法包装
最大的问题是:我们如何才能保留
bar
方法,而不实际保留实际方法?正如经常出现的那样,答案就在于函数式编程。我们将该方法作为实际的对象进行保存,并使用闭包(即块)来确保我们并且只有我们保存该对象:这非常干净:由于
old_bar
只是一个局部变量,它会在类主体末尾超出范围,并且不可能从任何地方访问它,甚至 使用反射!由于Module#define_method
需要一个块,并且块靠近其周围的词法环境(这就是为什么我们在这里使用define_method
而不是def
),it (并且仅它)仍然可以访问old_bar
,即使它超出了范围。简短说明:
这里我们将
bar
方法包装到UnboundMethod 中
方法对象并将其分配给局部变量old_bar
。这意味着,即使在bar
被覆盖之后,我们现在也有办法保留它。这有点棘手。基本上,在 Ruby(以及几乎所有基于单分派的 OO 语言)中,方法绑定到特定的接收者对象,在 Ruby 中称为
self
。换句话说:方法总是知道它被调用的对象是什么,它知道它的self
是什么。但是,我们直接从类中获取方法,它如何知道它的self
是什么?嗯,事实并非如此,这就是为什么我们需要
bind
我们的UnboundMethod
首先到一个对象,它将返回一个Method
我们可以调用的对象。 (UnboundMethod
无法被调用,因为它们在不知道自己的自身
的情况下不知道要做什么。)我们将它
绑定
到什么地方?我们只需将它绑定
到我们自己,这样它的行为就会完全像原来的bar
一样!最后,我们需要调用从
bind
返回的Method
。在 Ruby 1.9 中,有一些漂亮的新语法 (.()
),但如果您使用的是 1.8,则可以简单地使用调用
方法;无论如何,这就是.()
被翻译成的内容。以下是其他几个问题,其中解释了其中一些概念:
“肮脏的”猴子修补
alias_method
链我们在猴子修补中遇到的问题就是当我们覆盖方法的时候,方法就没有了,所以我们就不能再调用它了。所以,让我们制作一个备份副本!
问题是我们现在用多余的
old_bar
方法污染了命名空间。此方法将显示在我们的文档中,它将显示在我们的 IDE 中的代码完成中,它将显示在反射期间。另外,它仍然可以被调用,但想必我们猴子修补了它,因为我们一开始就不喜欢它的行为,所以我们可能不希望其他人调用它。尽管事实上它有一些不良特性,但不幸的是它已经通过 AciveSupport 的 变得流行起来
Module#alias_method_chain
。旁白:细化
如果您只需要少数情况下的不同行为特定的地方而不是遍及整个系统,您可以使用Refinements将猴子补丁限制在特定的范围内。我将在这里使用上面的
Module#prepend
示例进行演示:您可以在这个问题中看到使用 Refinements 的更复杂的示例:如何为特定方法启用猴子补丁?
废弃的想法
在 Ruby 社区确定
Module#prepend
之前,有多种不同的想法可供您选择有时可能会在较早的讨论中看到引用。所有这些都包含在Module#prepend
中。方法组合器
一种想法是来自 CLOS 的方法组合器的想法。这基本上是面向方面编程子集的一个非常轻量级的版本。
使用像您这样的语法
就可以“挂钩”
bar
方法的执行。然而,尚不清楚是否以及如何在
bar:after
中访问bar
的返回值。也许我们可以(ab)使用super
关键字?替换
before 组合器相当于在 mixin 前面添加一个重写方法,该方法在方法的最末尾调用
super
。同样,后组合器相当于在 mixin 前面添加一个重写方法,该方法在方法的最开始处调用 super。您还可以在调用
super
之前和执行一些操作,您可以多次调用super
,并且检索和操作super
code> 的返回值,使得prepend
比方法组合器更强大。和
old
关键字这个想法添加了一个类似于
super
的新关键字,它允许您以super<相同的方式调用覆盖方法/code> 允许您调用重写方法:
主要问题是它向后不兼容:如果您有名为
old
的方法,您将无法再呼唤它!在
prepend
ed mixin 中的重写方法中替换super
本质上与本提案中的old
相同。redef
关键字与上面类似,但我们没有为调用覆盖的方法添加新关键字并单独保留
def
,而是为重新定义方法。这是向后兼容的,因为无论如何目前的语法都是非法的:我们也可以在
redef
super 的含义,而不是添加两个新关键字>:替换
redef
方法相当于覆盖前置
混合中的方法。重写方法中的super
的行为类似于本提案中的super
或old
。EDIT: It has been 9 years since I originally wrote this answer, and it deserves some cosmetic surgery to keep it current.
You can see the last version before the edit here.
You can’t call the overwritten method by name or keyword. That’s one of the many reasons why monkey patching should be avoided and inheritance be preferred instead, since obviously you can call the overridden method.
Avoiding Monkey Patching
Inheritance
So, if at all possible, you should prefer something like this:
This works, if you control creation of the
Foo
objects. Just change every place which creates aFoo
to instead create anExtendedFoo
. This works even better if you use the Dependency Injection Design Pattern, the Factory Method Design Pattern, the Abstract Factory Design Pattern or something along those lines, because in that case, there is only place you need to change.Delegation
If you do not control creation of the
Foo
objects, for example because they are created by a framework that is outside of your control (like ruby-on-rails for example), then you could use the Wrapper Design Pattern:Basically, at the boundary of the system, where the
Foo
object comes into your code, you wrap it into another object, and then use that object instead of the original one everywhere else in your code.This uses the
Object#DelegateClass
helper method from thedelegate
library in the stdlib.“Clean” Monkey Patching
Module#prepend
: Mixin PrependingThe two methods above require changing the system to avoid monkey patching. This section shows the preferred and least invasive method of monkey patching, should changing the system not be an option.
Module#prepend
was added to support more or less exactly this use case.Module#prepend
does the same thing asModule#include
, except it mixes in the mixin directly below the class:Note: I also wrote a little bit about
Module#prepend
in this question: Ruby module prepend vs derivationMixin Inheritance (broken)
I have seen some people try (and ask about why it doesn’t work here on StackOverflow) something like this, i.e.
include
ing a mixin instead ofprepend
ing it:Unfortunately, that won’t work. It’s a good idea, because it uses inheritance, which means that you can use
super
. However,Module#include
inserts the mixin above the class in the inheritance hierarchy, which means thatFooExtensions#bar
will never be called (and if it were called, thesuper
would not actually refer toFoo#bar
but rather toObject#bar
which doesn’t exist), sinceFoo#bar
will always be found first.Method Wrapping
The big question is: how can we hold on to the
bar
method, without actually keeping around an actual method? The answer lies, as it does so often, in functional programming. We get a hold of the method as an actual object, and we use a closure (i.e. a block) to make sure that we and only we hold on to that object:This is very clean: since
old_bar
is just a local variable, it will go out of scope at the end of the class body, and it is impossible to access it from anywhere, even using reflection! And sinceModule#define_method
takes a block, and blocks close over their surrounding lexical environment (which is why we are usingdefine_method
instead ofdef
here), it (and only it) will still have access toold_bar
, even after it has gone out of scope.Short explanation:
Here we are wrapping the
bar
method into anUnboundMethod
method object and assigning it to the local variableold_bar
. This means, we now have a way to hold on tobar
even after it has been overwritten.This is a bit tricky. Basically, in Ruby (and in pretty much all single-dispatch based OO languages), a method is bound to a specific receiver object, called
self
in Ruby. In other words: a method always knows what object it was called on, it knows what itsself
is. But, we grabbed the method directly from a class, how does it know what itsself
is?Well, it doesn’t, which is why we need to
bind
ourUnboundMethod
to an object first, which will return aMethod
object that we can then call. (UnboundMethod
s cannot be called, because they don’t know what to do without knowing theirself
.)And what do we
bind
it to? We simplybind
it to ourselves, that way it will behave exactly like the originalbar
would have!Lastly, we need to call the
Method
that is returned frombind
. In Ruby 1.9, there is some nifty new syntax for that (.()
), but if you are on 1.8, you can simply use thecall
method; that’s what.()
gets translated to anyway.Here are a couple of other questions, where some of those concepts are explained:
“Dirty” Monkey Patching
alias_method
chainThe problem we are having with our monkey patching is that when we overwrite the method, the method is gone, so we cannot call it anymore. So, let’s just make a backup copy!
The problem with this is that we have now polluted the namespace with a superfluous
old_bar
method. This method will show up in our documentation, it will show up in code completion in our IDEs, it will show up during reflection. Also, it still can be called, but presumably we monkey patched it, because we didn’t like its behavior in the first place, so we might not want other people to call it.Despite the fact that this has some undesirable properties, it has unfortunately become popularized through AciveSupport’s
Module#alias_method_chain
.An aside: Refinements
In case you only need the different behavior in a few specific places and not throughout the whole system, you can use Refinements to restrict the monkey patch to a specific scope. I am going to demonstrate it here using the
Module#prepend
example from above:You can see a more sophisticated example of using Refinements in this question: How to enable monkey patch for specific method?
Abandoned ideas
Before the Ruby community settled on
Module#prepend
, there were multiple different ideas floating around that you may occasionally see referenced in older discussions. All of these are subsumed byModule#prepend
.Method Combinators
One idea was the idea of method combinators from CLOS. This is basically a very lightweight version of a subset of Aspect-Oriented Programming.
Using syntax like
you would be able to “hook into” the execution of the
bar
method.It is however not quite clear if and how you get access to
bar
’s return value withinbar:after
. Maybe we could (ab)use thesuper
keyword?Replacement
The before combinator is equivalent to
prepend
ing a mixin with an overriding method that callssuper
at the very end of the method. Likewise, the after combinator is equivalent toprepend
ing a mixin with an overriding method that callssuper
at the very beginning of the method.You can also do stuff before and after calling
super
, you can callsuper
multiple times, and both retrieve and manipulatesuper
’s return value, makingprepend
more powerful than method combinators.and
old
keywordThis idea adds a new keyword similar to
super
, which allows you to call the overwritten method the same waysuper
lets you call the overridden method:The main problem with this is that it is backwards incompatible: if you have method called
old
, you will no longer be able to call it!Replacement
super
in an overriding method in aprepend
ed mixin is essentially the same asold
in this proposal.redef
keywordSimilar to above, but instead of adding a new keyword for calling the overwritten method and leaving
def
alone, we add a new keyword for redefining methods. This is backwards compatible, since the syntax currently is illegal anyway:Instead of adding two new keywords, we could also redefine the meaning of
super
insideredef
:Replacement
redef
ining a method is equivalent to overriding the method in aprepend
ed mixin.super
in the overriding method behaves likesuper
orold
in this proposal.看一下别名方法,这是一种将方法重命名为新名称的方法。
有关更多信息和起点,请查看这篇替换方法文章(尤其是第一部分)。
Ruby API 文档 还提供了(不太详细的)示例。
Take a look at aliasing methods, this is kind of renaming the method to a new name.
For more information and a starting point take a look at this replacing methods article (especially the first part).
The Ruby API docs, also provides (a less elaborate) example.
将进行覆盖的类必须在包含原始方法的类之后重新加载,因此在将进行覆盖的文件中
require
它。The class that will make override must to be reloaded after class that contains the original method, so
require
it in the file that will make overrride.