假设我在一个类中修补一个方法,我如何从重写方法调用重写方法?也就是说,有点像超级

如。

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"

当前回答

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

要了解更多信息和起点,请参阅这篇替换方法的文章(特别是第一部分)。 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对象的创建,例如,因为它们是由一个超出你控制范围的框架创建的(比如ruby-on-rails),那么你可以使用包装器设计模式:

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中的委托库中的object# DelegateClass helper方法。

“清洁”猴子修补

模块#prepend: Mixin Prepending

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

模块#prepend被添加来或多或少地支持这个用例。模块#prepend做的事情和模块#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'

注意:在这个问题中,我也写了一些关于模块#prepend的内容:Ruby模块的prepend与派生

Mixin继承(损坏)

我见过一些人尝试(并询问为什么它不能在StackOverflow上工作)这样的东西,即包括一个mixin,而不是前置它:

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总是首先被找到。

包装方法

最大的问题是,我们如何在不保留实际方法的情况下,坚持使用条形方法呢?答案就像经常发生的那样,在于函数式编程。我们将方法作为一个实际对象来获取,然后使用闭包(即块)来确保我们且只有我们拥有该对象:

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只是一个局部变量,它将在类主体的末尾超出范围,并且不可能从任何地方访问它,即使使用反射!由于模块#define_method接受一个块,并且块在它们周围的词汇环境中关闭(这就是为什么我们在这里使用define_method而不是def),它(而且只有它)仍然可以访问old_bar,即使它超出了作用域。

简短说明:

old_bar = instance_method(:bar)

在这里,我们将bar方法包装到UnboundMethod方法对象中,并将其分配给局部变量old_bar。这意味着,我们现在有一种方法来保留bar,即使它已经被覆盖。

old_bar.bind(self)

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

好吧,它没有,这就是为什么我们需要先将我们的UnboundMethod绑定到一个对象,这将返回一个我们可以调用的Method对象。(UnboundMethods不能被调用,因为它们在不知道自己的情况下不知道该做什么。)

我们把它和什么结合呢?我们只需将它绑定到我们自己,这样它就会像原来的棒一样!

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

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

如何在Ruby中引用函数? Ruby的代码块与c#的lambda表达式相同吗?

“肮脏的”猴子修补

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的模块#alias_method_chain普及起来的。

题外话:细化

如果您只需要在一些特定的地方而不是整个系统中使用不同的行为,您可以使用Refinements将monkey补丁限制在特定的范围内。我将在这里使用上面的模块#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的例子:如何为特定的方法启用monkey patch ?


放弃的想法

在Ruby社区确定Module#prepend之前,有很多不同的想法,你可能偶尔会在以前的讨论中看到它们。所有这些都包含在模块#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

更换

before组合子相当于在mixin前加上一个覆盖方法,该方法在方法的最后调用super。类似地,after组合子相当于在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

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

老字

这个想法增加了一个类似于super的新关键字,它允许你调用被覆盖的方法,就像super让你调用被覆盖的方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

主要的问题是它是向后不兼容的:如果你有一个叫做old的方法,你将不再能够调用它!

更换

前置mixin中覆盖方法中的Super本质上与本建议中的旧方法相同。

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'

更换

重新定义方法相当于重写预先添加的mixin中的方法。重写方法中的Super在此提议中表现为Super或old。

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

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