ruby - 当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗?

标签 ruby monkeypatching

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

class Foo
  def bar()
    "Hello"
  end
end 

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

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

最佳答案

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

可以看到编辑前的最后一个版本here .

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

避免猴子补丁

遗产

所以,如果可能的话,你应该更喜欢这样的事情:

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 Pattern,效果会更好。 , Factory Method Design Pattern , Abstract Factory Design Pattern或类似的东西,因为在这种情况下,只有你需要改变的地方。

代表团

如果不控制Foo的创建对象,例如因为它们是由不受您控制的框架创建的(例如 ),那么您可以使用 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对象进入您的代码,您将其包装到另一个对象中,然后在代码中的其他任何地方使用该对象而不是原始对象。

这使用 Object#DelegateClass 来自 delegate 的辅助方法标准库中的库。

“干净”的猴子补丁

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 module prepend vs derivation

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

不幸的是,这行不通。这是一个好主意,因为它使用继承,这意味着你可以使用 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(以及几乎所有基于单调度的面向对象语言)中,一个方法绑定(bind)到一个特定的接收器对象,称为 self。在 ruby 。换句话说:一个方法总是知道它被调用的对象是什么,它知道它的self是。但是,我们直接从一个类中获取方法,它怎么知道它的self是?

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

我们该怎么办 bind它到?我们只是bind对我们自己来说,这样它的行为就会和原来的完全一样 bar将有!

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

以下是一些其他问题,其中解释了其中一些概念:
  • How do I reference a function in Ruby?
  • Is Ruby’s code block same as C♯’s lambda expression?

  • “脏”猴子补丁

    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 将猴子补丁限制在特定范围内。我将在这里使用 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!
    

    你可以在这个问题中看到一个更复杂的使用 Refinements 的例子:How to enable monkey patch for specific method?

    废弃的想法

    之前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
    

    您将能够“ Hook ”bar 的执行方法。

    然而,目前尚不清楚您是否以及如何访问 bar bar:after内的返回值.也许我们可以(ab)使用 super关键词?
    class Foo
      def bar
        'Hello'
      end
    end 
    
    class Foo
      def bar:after
        super + ' World'
      end
    end
    

    替代品

    before 组合子相当于 prepend使用调用 super 的覆盖方法创建 mixin在方法的最后。同样,after 组合子等价于 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
    


    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 的方法,您将无法再调用它!

    替代品
    superprepend 中的覆盖方法中ed mixin 与 old 基本相同在这个提议中。
    redef关键词

    与上面类似,但不是添加一个新的关键字来调用被覆盖的方法并留下 def单独,我们添加了一个新的关键字来重新定义方法。这是向后兼容的,因为目前的语法无论如何都是非法的:
    class Foo
      def bar
        'Hello'
      end
    end 
    
    class Foo
      redef bar
        old + ' World'
      end
    end
    
    Foo.new.bar # => 'Hello World'
    

    除了添加两个新关键字之外,我们还可以重新定义 super 的含义。内redef :
    class Foo
      def bar
        'Hello'
      end
    end 
    
    class Foo
      redef bar
        super + ' World'
      end
    end
    
    Foo.new.bar # => 'Hello World'
    

    替代品
    redef在一个方法中等效于覆盖一个 prepend 中的方法ed混合。 super在覆盖方法中的行为类似于 superold在这个提议中。

    关于ruby - 当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4470108/

    相关文章:

    python-3.x - Python - 猴子补丁失败,为什么?

    java - JRuby 到 jar 可执行文件

    ruby - 如何从整数中得到 10s、100s、1000s?

    ruby - 如何在 cucumber 步骤定义之外使用 rspec-expectations

    ruby - 使用 Padrino、Sass 和 Slim 从布局链接到 CSS

    ruby-on-rails - 如何在运行rails控制台时手动设置ENV变量

    ruby - 如何为 Ruby 编写猴子补丁?

    oop - 猴子补丁还是不猴子?

    python - 在 Python 中修补一个类的所有实例

    javascript - 我可以在编写 TypeScript 单元测试时对依赖项进行猴子补丁吗