问题

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

例如

class Foo
  def bar()
    "Hello"
  end
end 

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

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


解决方法

编辑:从我最初写这个答案已经5年了,它应该进行整容手术以保持最新.

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


您无法通过名称或关键字调用覆盖的方法.这是为什么应该避免猴子修补和继承优先的很多原因之一,因为显然你可以 调用方法.

Avoiding Monkey Patching

Inheritance

因此,如果可能,您应该喜欢这样:

class Foo
  def bar
    'Hello'
  end
end 

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

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

如果你控制 Foo 对象的创建,这个工作.只需更改创建 Foo 的每个地方,而不是创建一个 ExtendedFoo .如果您使用依赖关系注入设计模式,工厂方法设计模式,抽象工厂设计模式或沿着这些线条的东西,因为在这种情况下,只有你需要改变的地方.

Delegation

如果您控制 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'

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

这使用 Object#来自 委托 辅助方法> library in the stdlib.

"Clean" Monkey Patching

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'

注意:我还在这个问题中写了一些关于 Module#prepend 的信息: Ruby模块前导与衍生

Mixin Inheritance (broken)

我已经看到一些人尝试(并询问为什么它不工作这里在StackOverflow)这样,例如 include mixin而不是 prepend it:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

不幸的是,这不会工作.这是一个好主意,因为它使用继承,这意味着你可以使用 super .但是, Module#include insert mixin 在继承层次结构中的类,这意味着 FooExtensions#bar 将永远不会被调用(如果代码> super 实际上不会引用 Foo#bar ,而是 Object#bar / code>将始终被找到.

Method Wrapping

最大的问题是:我们如何坚持 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 只是一个局部变量,它将在类体的末尾超出范围,并且不可能从任何地方访问它,甚至使用反射!由于 Module#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 .换句话说:一个方法总是知道它被调用的对象,它知道它的 self 是什么.但是,我们直接从一个类中抓取方法,它是如何知道它的 self 是什么?

好吧,它没有,这就是为什么我们需要 <代码> bind 我们的 UnboundMethod 到一个对象,它将返回一个 Method 对象,然后我们可以调用. ( UnboundMethod s不能被调用,因为他们不知道该怎么做,而不知道他们 self .)

我们绑定到什么?我们只需将 bind 绑定到自己,就像原来 bar 一样,

最后,我们需要调用从 bind 返回的 Method .在Ruby 1.9中,有一些漂亮的新语法(.()),但是如果你在1.8,你可以使用 call method;这是.()得到翻译.

这里有几个其他问题,其中一些概念会解释:

"Dirty" Monkey Patching

alias_method chain

我们使用猴子修补的问题是,当我们重写方法时,该方法就不见了,所以我们不能再调用它了.所以,让我们做一个备份副本!

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的 Module#alias_method_chain .

An aside: Refinements

如果你只需要在几个特定的​​地方,而不是整个系统的不同行为,你可以使用细化来限制猴子补丁到一个特定的范围.我将使用上面的 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!

您可以在这个问题中查看使用改进的更复杂的示例:如何为特定方法启用猴子补丁?


Abandoned ideas

在Ruby社区解决 module#prepend 之前,有多个不同的想法浮动,你可能偶尔看到在旧的讨论中引用.所有这些都归入 Module#prepend .

Method Combinators

一个想法是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 的返回值.也许我们可以(ab)使用 super 关键字?

class Foo
  def bar
    'Hello'
  end
end 

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

Replacement

前组合器相当于在方法的结尾调用 super 的覆盖方法同样,后组合器相当于在方法的第开始调用 super 的重写方法 p>

您还可以在调用 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

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 keyword

这个想法添加了一个类似于 super 的新关键字,它允许您以相同的方式调用覆盖的方法 super 覆盖方法:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

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

Replacement

prepend ed mixin中的覆盖方法中的基本上与 old

redef keyword

与上述类似,我们为重新定义添加了一个新关键字,而不是为调用 em>方法.这是向后兼容的,因为语法目前是非法的:

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'

Replacement

redef 方法相当于覆盖 prepend ed mixin中的方法.在此提案中,覆盖方法中的 super 行为类似于 super old .




相关问题推荐