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

夙愿已清 提交于 2020-02-27 13:01:54

假设我是猴子在类中修补方法,那么如何从覆盖方法中调用覆盖方法? 即有点像super

例如

class Foo
  def bar()
    "Hello"
  end
end 

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

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

#1楼

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


#2楼

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

有关更多信息和起点,请参阅此替代方法文章 (尤其是第一部分)。 Ruby API docs ,也提供了(不太复杂)的示例。


#3楼

编辑 :距离我最初写这个答案已有9年了,它值得进行一些整容手术以保持最新。

您可以在此处查看修改之前的最新版本。


您不能通过名称或关键字来调用覆盖的方法。 这就是为什么应该避免猴子修补而首选继承的许多原因之一,因为显然您可以调用重写方法。

避免猴子打补丁

遗产

因此,如果可能的话,您应该喜欢这样的东西:

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 )创建的,则可以使用Wrapper设计模式

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 :混入前

以上两种方法需要更改系统以避免猴子补丁。 本部分显示了猴子修补的首选且侵入性最小的方法,如果不应该选择更换系统,则不行。

添加了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#prependRuby module prepend vs derivation

Mixin继承(中断)

我见过有人尝试(并询问为什么它在StackOverflow上不起作用)这样的事情,即include混合而不是在其prepend

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

方法包装

最大的问题是:如何在不实际使用实际方法的情况下坚持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 bind到一个对象,这将返回一个我们可以调用的Method对象。 ( UnboundMethod s不能被调用,因为他们不知道该怎么不知道自己做self 。)

我们bindbind到什么? 我们只需bindbind到我们自己,那样它的行为就将完全像原始bar一样!

最后,我们需要调用从bind返回的Method 。 在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'

问题是我们现在已经用多余的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!

您可以在以下问题中看到一个使用“优化”的更复杂的示例: 如何为特定方法启用猴子补丁?


被遗弃的想法

在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

替代

在之前组合子相当于prepend荷兰国际集团一个mixin与调用方法覆盖super在方法的尽头 。 同样,组合子后相当于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的新关键字,它允许您调用覆盖方法,就像super可以调用覆盖方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

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

替代

prepend mixin中的替代方法中的super与该建议中的old基本相同。

redef关键字

与上面类似,但是我们没有添加新的关键字来调用被覆盖的方法而让def独立存在,而是添加了新的关键字来重新定义方法。 这是向后兼容的,因为目前语法仍然是非法的:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

除了添加两个新的关键字,我们还可以重新定义super inside redef的含义:

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的这个建议。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!