当猴子修补方法时,你能从新实现调用重写方法吗?

Jam*_*rth 422 ruby monkeypatching

假设我是猴子修补类中的方法,我怎么能从覆盖方法调用重写方法?就像有点像super

例如

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
Run Code Online (Sandbox Code Playgroud)

Jör*_*tag 1129

编辑:我最初写这个答案已有5年了,值得一些整容手术来保持最新状态.

您可以在此处编辑之前查看最新版本.


您无法通过名称或关键字调用覆盖的方法.这是为什么应该避免使用猴子补丁并且继承是首选的众多原因之一,因为显然你可以调用重写方法.

避免猴子补丁

遗产

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

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'
Run Code Online (Sandbox Code Playgroud)

如果您控制Foo对象的创建,则此方法有效.只需更改创建一个Footo的每个地方,而不是创建一个ExtendedFoo.如果您使用依赖注入设计模式,工厂方法设计模式,抽象工厂设计模式或其他方面的东西,这样可以更好地工作,因为在这种情况下,只有您需要更改的位置.

代表团

如果您控制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'
Run Code Online (Sandbox Code Playgroud)

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

这使用stdlib中库的Object#DelegateClass辅助方法delegate.

"清洁"猴子补丁

Module#prepend:Mixin Prepending

上述两种方法需要更改系统以避免猴子修补.本节显示了猴子修补的首选和最少侵入性方法,如果更改系统不是一个选项.

Module#prepend被添加以支持或多或少正是这个用例.Module#prepend做同样的事情Module#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'
Run Code Online (Sandbox Code Playgroud)

注意:我还在Module#prepend这个问题中写了一些内容:Ruby模块prepend与派生

Mixin继承(破)

我看到有些人尝试(并询问为什么它在StackOverflow上不起作用)这样的事情,即include使用mixin而不是prepend它:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end
Run Code Online (Sandbox Code Playgroud)

不幸的是,这不会奏效.这是一个好主意,因为它使用继承,这意味着你可以使用super.但是,在继承层次结构Module#include中将mixin插入到类的上方,这意味着FooExtensions#bar永远不会被调用(如果它调用,则super实际上不会引用,Foo#bar而是Object#bar不存在),因为Foo#bar将始终首先找到它.

方法包装

最大的问题是:如何在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'
Run Code Online (Sandbox Code Playgroud)

这非常干净:因为old_bar它只是一个局部变量,它将超出类体末端的范围,即使使用反射也无法从任何地方访问它!而且由于Module#define_method需要一个块,并且它们在周围的词汇环境中闭合(这就是我们使用define_method而不是在def这里的原因),(并且只有它)仍然可以访问old_bar,即使它已经超出了范围.

简短说明:

old_bar = instance_method(:bar)
Run Code Online (Sandbox Code Playgroud)

这里我们将bar方法包装到UnboundMethod方法对象中并将其分配给局部变量old_bar.这意味着,bar即使在被覆盖之后,我们现在仍然可以坚持下去.

old_bar.bind(self)
Run Code Online (Sandbox Code Playgroud)

这有点棘手.基本上,在Ruby(以及几乎所有基于单调度的OO语言)中,方法绑定到特定的接收器对象,self在Ruby中调用.换句话说:一个方法总是知道它被调用的对象,它知道它self是什么.但是,我们直接从一个类中抓取了这个方法,它self是如何知道它的呢?

好了,这不,这就是为什么我们需要bind我们的UnboundMethod一个对象首先,它会返回一个Method对象,就可以调用.(UnboundMethods不能被称为,因为他们不知道自己该怎么做self.)

我们bind该怎么做?我们只是bind它自己,这样它的行为正是像原来bar必须!

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

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

"肮脏的"猴子补丁

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'
Run Code Online (Sandbox Code Playgroud)

这个问题是我们现在用一个多余的old_bar方法污染了命名空间.这个方法将在我们的文档中显示,它将显示在我们的IDE中的代码完成中,它将在反射期间显示.此外,它仍然可以被调用,但可能我们猴子修补它,因为我们首先不喜欢它的行为,所以我们可能不希望其他人称之为它.

尽管事实上它具有一些不良特性,但不幸的是它已经通过AciveSupport得以普及Module#alias_method_chain.

旁白:改进

如果您只需要在几个特定位置而不是整个系统中使用不同的行为,则可以使用"优化"将猴子补丁限制到特定范围.我将使用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!
Run Code Online (Sandbox Code Playgroud)

您可以在此问题中看到更复杂的使用优化的示例:如何为特定方法启用Monkey补丁?


被遗弃的想法

在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
Run Code Online (Sandbox Code Playgroud)

你将能够"挂钩" bar方法的执行.

但是,您是否以及如何获得其中bar的返回值并不十分清楚bar:after.也许我们可以(ab)使用super关键字?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end
Run Code Online (Sandbox Code Playgroud)

替换

的前组合子相当于prepend荷兰国际集团一个混合与调用方法覆盖super在最端部的方法的.同样地,后组合器等同于prepend使用在方法super的最开始时调用的重写方法的mixin .

你也可以在调用之前之后做一些事情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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

old 关键词

这个想法增加了一个新的关键字类似super,它允许你调用覆盖方法以同样的方式super,您可以调用重载的方法:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Run Code Online (Sandbox Code Playgroud)

这个问题的主要问题是它向后兼容:如果你有方法调用old,你将无法再调用它!

替换

superprepended mixin 中的一个重要方法与old本提案中的基本相同.

redef 关键词

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

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Run Code Online (Sandbox Code Playgroud)

我们还可以重新定义内部的含义,而不是添加两个新的关键字:superredef

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Run Code Online (Sandbox Code Playgroud)

替换

redefining方法相当于覆盖prepended mixin中的方法.super在重写方法中表现得像superold在此提案中.

  • 你在哪里找到`old`和`redef`?我的2.0.0没有它们.啊,很难不错过*没有进入Ruby的其他竞争想法:* (5认同)
  • 自从我开始使用 ruby​​ 和 rails 以来,一直在寻找这样的补丁的解释。很好的答案!我唯一缺少的是关于 class_eval 与重新开设课程的说明。这是:http://stackoverflow.com/a/10304721/188462 (2认同)

Veg*_*ger 12

看一下别名方法,这有点将方法重命名为新名称.

有关更多信息和起点,请参阅此替换方法文章(尤其是第一部分).的红宝石API文档,还提供了(a不太精细的)的例子.