当猴子修补一个方法时,你可以从新的实现中调用重写的方法吗?

说我是猴子补丁在类中的方法,我怎么能从重写的方法调用重写的方法? 即有点像super

例如

 class Foo def bar() "Hello" end end class Foo def bar() super() + " World" end end >> Foo.new.bar == "Hello World" 

编辑 :从我最初写这个答案已经5年了,它值得一些整容手术,以保持当前。

在编辑之前,您可以看到最后一个版本。


您不能通过名称或关键字调用覆盖的方法。 这就是为什么应该避免使用猴子修补和inheritance优先的原因之一,因为显然你可以调用重写的方法。

避免猴子补丁

遗产

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

 class Foo def bar 'Hello' end end class ExtendedFoo < Foo def bar super + ' World' end end ExtendedFoo.new.bar # => 'Hello World' 

这个工作,如果你控制Foo对象的创build。 只要改变创buildFoo每个地方,而不是创build一个ExtendedFoo 。 如果你使用dependency injectiondevise模式 , 工厂方法devise模式 , 抽象工厂devise模式或者其他方法 ,那么这样做会更好,因为在这种情况下,只有你需要改变的地方。

代表团

如果您控制Foo对象的创build,例如因为它们是由您的控制之外的框架(例如ruby-on-rails )创build的,那么您可以使用Wrapper Design Pattern :

 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

上述两种方法需要改变系统以避免猴子修补。 这部分显示了猴子补丁的首选和最less侵入的方法,应该改变系统不是一个选项。

Module#prepend ,以支持或多或less这个用例。 Module#prependModule#include的function相同,除了混合在类的正下方的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与派生

Mixininheritance(破坏)

我曾经见过一些人尝试(并询问为什么它不能在StackOverflow中工作),就像这样, include一个mixin而不是prepend

 class Foo def bar 'Hello' end end module FooExtensions def bar super + ' World' end end class Foo include FooExtensions end 

不幸的是,这是行不通的。 这是一个好主意,因为它使用inheritance,这意味着你可以使用super 。 但是, Module#include在inheritance层次结构的类的上面插入了mixin,这意味着FooExtensions#bar永远不会被调用(如果调用, super将不会实际引用Foo#bar而是引用Object#bar不存在),因为Foo#bar始终会被首先find。

方法包装

最大的问题是:我们怎样才能坚持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只是一个局部variables,它将在类体的末尾超出范围, 即使使用reflection也无法从任何地方访问它! 而且,由于Module#define_method占用了一个块,并且阻塞了它们周围的词法环境(这就是为什么我们在这里使用define_method而不是def )的原因, 只有它)仍然可以访问old_bar ,即使它已经出去范围。

简短的解释:

 old_bar = instance_method(:bar) 

在这里,我们将bar方法封装到UnboundMethod方法对象中,并将其分配给本地variablesold_bar 。 这意味着,我们现在有一种方法可以在被覆盖之后继续保持bar

 old_bar.bind(self) 

这有点棘手。 基本上,在Ruby(几乎所有基于单个调度的OO语言)中,方法都绑定到一个特定的接收者对象,在Ruby中称为self 。 换句话说:一个方法总是知道它被调用的是什么对象,它知道self是什么。 但是,我们直接从课堂上抓了这个方法,它怎么知道self是什么?

嗯,这不是,这就是为什么我们需要先bind我们的UnboundMethod到一个对象,然后返回一个我们可以调用的Method对象。 ( UnboundMethod不能被调用,因为他们不知道自己该怎么做。)

我们把它bind到什么地方? 我们简单地把它bind在自己身上,这样它就会像原来的bar一样!

最后,我们需要调用从bind返回的Method 。 在Ruby 1.9中,这个( .() )有一些漂亮的新语法,但是如果你使用1.8,你可以简单地使用call方法。 这就是.()无论如何翻译。

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

  • 我如何在Ruby中引用一个函数?
  • Ruby的代码块是否和C的lambdaexpression式相同?

“肮脏的”猴子补丁

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中,它会在reflection过程中出现。 而且,它还是可以被调用的,但是大概我们用猴子来修补它,因为我们不喜欢它的行为,所以我们可能不希望别人叫它。

尽pipe这有一些不良的特性,但不幸的是它已经通过AciveSupport的Module#alias_method_chain变得stream行起来。

一边: 修饰

如果您只需要某些特定位置而不是整个系统中的不同行为,则可以使用“优化”将猴子修补程序限制到特定范围。 我将在这里使用上面的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内的回报价值,并不十分清楚bar:after 。 也许我们可以(ab)使用super关键字?

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

替代

之前的combinator相当于在方法的最后调用super方法的prepend方法。 同样,后组合器相当于在方法一开始就用一个super方法来调用super方法。

你也可以在调用super之前之后做一些事情,你可以多次调用super ,并且检索和操作super的返回值,使得比方法combinator更加强大。

 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方法,你将不能再调用它!

替代

super在一个prepend混合中的压倒一切的方法在本提案中基本上与old的相同。

redef关键字

与上面类似,不是添加一个新的关键字来调用被覆盖的方法,而是保留def ,我们添加一个新的关键字来重新定义方法。 这是向后兼容的,因为目前的语法是非法的:

 class Foo def bar 'Hello' end end class Foo redef bar old + ' World' end end Foo.new.bar # => 'Hello World' 

而不是增加两个新的关键字,我们也可以重新定义里面的redefsuper redef

 class Foo def bar 'Hello' end end class Foo redef bar super + ' World' end end Foo.new.bar # => 'Hello World' 

替代

redef一个方法就等同于在一个prepend redef中重写该方法。 super方法在这个提议中performance得如同super或者old一样。

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

有关更多信息和出发点,请参阅此replace方法文章 (特别是第一部分)。 Ruby API文档也提供了(不太复杂的)示例。

要覆盖的类必须在包含原始方法的类之后重新加载,所以require它在覆盖的文件中。