ruby 当猴子修补一个示例方法时,你能从新的实现中调用被覆盖的方法吗?

hts6caw3  于 2023-01-16  发布在  Ruby
关注(0)|答案(3)|浏览(153)

假设我正在修补类中的一个方法,我如何从覆盖方法调用被覆盖的方法呢?例如,有点像super
例如

class Foo
  def bar()
    "Hello"
  end
end 

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

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

q8l4jmvw1#

编辑:我最初写这个答案已经9年了,它值得做一些整容手术来保持它的最新状态。

您可以看到编辑here之前的最后一个版本。
你不能通过名字或关键字调用 * overwritted * 方法,这就是为什么要避免猴子修补而选择继承的原因之一,因为很明显你可以调用 * overwritted * 方法。

避免猴子打补丁

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

class Foo
  def bar
    'Hello'
  end
end 

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

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

如果你控制Foo对象的创建,这是可行的。只要改变每一个创建Foo的地方,而不是创建ExtendedFoo。如果你使用Dependency Injection Design PatternFactory Method Design PatternAbstract Factory Design Pattern或类似的东西,这会更好,因为在这种情况下,你只需要改变一个地方。

代表团

如果您 * 不 * 控制Foo对象的创建,例如,因为它们是由您无法控制的框架创建的(例如ruby-on-rails),则可以使用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对象进入代码的地方,您将它 Package 到另一个对象中,然后在代码的其他地方使用 that 对象,而不是原来的对象。
这将使用stdlib中delegate库中的Object#DelegateClass辅助方法。

“干净”的猴子贴片

Module#prepend:Mixin前置

以上两种方法都需要更改系统以避免猴子打补丁。本节介绍了猴子打补丁的首选和最小侵入性方法,如果更改系统不是一个选项。
添加Module#prepend是为了或多或少地支持这种用例。Module#prependModule#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继承(已中断)

我看到一些人尝试过(并询问为什么它在StackOverflow上不起作用)类似这样的东西,即include ing一个mixin而不是prepend ing它:

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

方法 Package

最大的问题是:我们怎样才能保留bar方法,而不实际保留一个 actual method 呢?答案就在函数式编程中,就像它经常做的那样。我们把方法作为一个实际的 object 来保留,并且我们使用一个闭包(即一个块)来确保我们 * 并且只有我们 * 保留那个对象:

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方法 Package 成一个UnboundMethod方法对象,并把它赋给局部变量old_bar,这意味着,我们现在有了一种方法,即使bar被覆盖了,我们也能保留它。

old_bar.bind(self)

这有点棘手,基本上,在Ruby中(以及几乎所有基于单分派的OO语言中),一个方法被绑定到一个特定的接收器对象,在Ruby中称为self。换句话说:一个方法总是知道它被调用的对象是什么,它知道它的self是什么,但是,我们直接从一个类中获取这个方法,它怎么知道它的self是什么呢?
但事实并非如此,这就是为什么我们需要先将bind我们的UnboundMethod转换为一个对象,它将返回一个Method对象,然后我们可以调用它(UnboundMethod不能被调用,因为它们不知道在不知道self的情况下该做什么)。
我们把它bind到什么地方呢?我们只是把它bind到我们自己,这样它的行为就和原来的bar一样!
最后,我们需要调用从bind返回的Method,在Ruby 1.9中,有一些漂亮的新语法(.()),但是如果你在1.8中,你可以简单地使用call方法;这就是.()被翻译成的东西。
下面是其他几个问题,其中一些概念将得到解释:

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

“肮脏”的猴子补丁

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的代码完成中,它将出现在反射期间。此外,它仍然可以被调用,但可能我们已经修补了它,因为我们一开始就不喜欢它的行为,所以我们可能不希望其他人调用它。
尽管它有一些不理想的特性,但不幸的是,它通过AciveSupport的Module#alias_method_chain而流行起来。

旁白:Refinements

如果您只需要在几个特定的地方而不是整个系统中的不同行为,您可以使用Refinements将monkey补丁限制在特定的范围内。我将在这里使用上面的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:after中访问bar的返回值还不是很清楚。也许我们可以(ab)使用super关键字?

class Foo
  def bar
    'Hello'
  end
end 

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

更换

before组合符等价于prepend使用一个覆盖方法在mixin的"末尾"调用super;同样,after组合符等价于prepend使用一个覆盖方法在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

以及

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的新关键字,它允许您调用 * overwritted * 方法,就像super允许您调用 * overwritted * 方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

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

更换

prepend ED混合中的重写方法中的super与本提议中的old基本相同。

redef关键字

与上面类似,但我们没有为 * call * 覆盖的方法添加一个新的关键字,而不去管def,而是为 * redefining * 方法添加了一个新的关键字,这是向后兼容的,因为当前的语法无论如何都是非法的:

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'

更换

redef定义方法等同于在prepend编辑的mixin中覆盖该方法。覆盖方法中的super的行为类似于本建议中的superold

a8jjtwal

a8jjtwal2#

看一下别名方法,这是一种重命名方法为一个新的名称。
要了解更多信息和起点,请查看replacing methods article(尤其是第一部分)。Ruby API docs也提供了(一个不太详细的)示例。

tag5nh1u

tag5nh1u3#

将进行覆盖的类必须在包含原始方法的类之后重新加载,因此require它在将进行覆盖的文件中。

相关问题