当monkey修补实例方法时,是否可以从新实现中调用重写的方法?

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

例如

类Foo
def bar()
“你好”
终止
终止
福班
def bar()
超级()+“世界”
终止
终止
&燃气轮机&燃气轮机;Foo.new.bar==“你好,世界”

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

您可以在此处查看编辑之前的最后一个版本


不能按名称或关键字调用重写的方法。这就是为什么应该避免使用猴子补丁,而应该首选继承的原因之一,因为显然您可以调用覆盖的方法

避免猴子打补丁

继承权

因此,如果可能的话,您应该选择这样的方式:

类Foo
def棒
“你好”
终止
终止
类扩展foo<福
def棒
超级+‘世界’
终止
终止
ExtendedFoo.new.bar#=>'你好,世界杯

如果您控制Foo对象的创建,这是可行的。只需将创建Foo的每个位置更改为创建ExtendedFoo。如果您使用依赖项注入设计模式、工厂方法设计模式、抽象工厂设计模式或类似的设计模式,这一点会更好,因为在这种情况下,您只需要更改位置

授权

如果不控制Foo对象的创建,例如,因为它们是由您无法控制的框架(例如ruby on rails)创建的,那么您可以使用包装器设计模式:

要求“委托”
福班
def棒
“你好”
终止
终止
类WrappedFoo<委派类(Foo)
def初始化(包装的_foo)
超级的
终止
def棒
超级+‘世界’
终止
终止
foo=foo.new#这实际上不在您的代码中,它来自其他地方
wrapped_foo=WrappedFoo.new(foo)#这由您控制
包裹的_foo.bar#=>'你好,世界杯

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

这使用stdlib中的delegate库中的Object#DelegateClasshelper方法

“干净”猴子修补

模块#预结束:混合预结束

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

添加了Module#prepend以或多或少地支持此用例Module#prependModule#include的作用相同,只是它直接混入类下方的mixin

类Foo
def棒
“你好”
终止
终止
模块扩展
def棒
超级+‘世界’
终止
终止
福班
预加食物扩展
终止
Foo.new.bar#=>'你好,世界杯

注意:在这个问题中,我还写了一点关于Module#prepend:Ruby Module prepend vs derivation

Mixin继承(断开)

我曾看到一些人尝试(并询问为什么它在StackOverflow上不起作用)类似这样的东西,即包含混音而不是预编混音:

类Foo
def棒
“你好”
终止
终止
模块扩展
def棒
超级+‘世界’
终止
终止
福班
包括FooExtensions
终止

不幸的是,这行不通。这是个好主意,因为它使用继承,这意味着您可以使用super。但是,Module#include在继承层次结构中的类上方插入mixin,这意味着永远不会调用footextensions#bar(如果调用了super实际上不会引用Foo#bar,而是引用不存在的Object#bar),因为Foo#bar总是最先找到的

方法包装

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

类Foo
def棒
“你好”
终止
终止
福班
旧条=实例条方法(:条)
定义_方法(:bar)do
旧酒吧。绑定(自我)。()+“世界”
终止
终止
Foo.new.bar#=>'你好,世界杯

这是非常清楚的:因为old_bar只是一个局部变量,它将超出类主体末尾的范围,并且不可能从任何地方访问它,甚至使用反射来访问它!由于Module#define_method需要一个块,并且块靠近它们周围的词汇环境(这就是为什么我们使用define_method而不是def),因此它(并且只有它)仍然可以访问旧的,即使它已经超出了范围

简要说明:

old_bar=实例_方法(:bar)

这里我们将bar方法包装到UnboundMethod方法对象中,并将其分配给局部变量old\u bar。这意味着,我们现在有了一种方法,即使在被覆盖后,也可以保持

旧条绑定(自)

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

事实并非如此,这就是为什么我们需要首先将我们的UnboundMethod绑定到一个对象,它将返回一个方法对象,然后我们可以调用该对象。(UnboundMethods无法调用,因为如果不知道自己的自我,他们不知道该做什么)

我们将它绑定到什么?我们只需将它绑定到我们自己,这样它的行为就会像原来的条一样

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

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

  • 如何在Ruby中引用函数
  • Ruby的代码块与C相同吗♯’什么是lambda表达式

“脏”猴子补丁

alias\u方法chain

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

类Foo
def棒
“你好”
终止
终止
福班
别名\方法:旧\条,:条
def棒
旧酒吧+“世界”
终止
终止
Foo.new.bar#=>'你好,世界杯
Foo.new.old#u bar#=>'你好’

问题是,我们现在用一个多余的旧的\u bar方法污染了名称空间。这种方法将出现在我们的文档中,它将出现在我们的IDE中的代码完成中,它将出现在反射过程中。而且,它仍然可以被称为,但我们可能是猴子修补了它,因为我们一开始不喜欢它的行为,所以我们可能不希望其他人叫它

尽管它有一些不受欢迎的特性,但不幸的是,它已通过AciveSupport的模块#别名(alias)方法(chain)得到普及

旁白:改进

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

类Foo
def棒
“你好”
终止
终止
模块扩展foo
模块扩展
def棒
超级+‘世界’
终止
终止
精炼福多
预加食物扩展
终止
终止
Foo.new.bar#=>'你好’
#我们还没有激活我们的精炼!
使用ExtendedFoo
#激活我们的精致
Foo.new.bar#=>'你好,世界杯
#就在那里!

在这个问题中,您可以看到一个更复杂的使用改进的示例:如何为特定方法启用monkey patch


废弃的想法

在Ruby社区决定使用Module#prepend之前,有很多不同的想法,您可能偶尔会在以前的讨论中看到这些想法。所有这些都包含在模块#prepend

方法组合子

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

使用类似

类Foo
def bar:之前
#调用bar时,将始终在bar之前运行
终止
def bar:之后
#调用bar时,将始终在bar之后运行
#可能无法访问和/或更改条形图的返回值
终止
终止

您将能够“钩住”执行bar方法

然而,您是否以及如何访问

发表评论