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对象的创建,例如因为它们是由您无法控制的框架(例如ruby-on-rails)创建的,那么您可以使用包装器设计模式:
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与派生
我看到有些人尝试(并询问为什么它在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,你将无法再调用它!
super在prepended 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在重写方法中表现得像super或old在此提案中.
| 归档时间: |
|
| 查看次数: |
79269 次 |
| 最近记录: |