当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗? [英] When monkey patching an instance method, can you call the overridden method from the new implementation?
问题描述
假设我是猴子修补类中的一个方法,我如何从覆盖方法调用覆盖方法?IE.有点像 super
例如
class Foo定义栏()你好"结尾结尾Foo类定义栏()super() + 世界"结尾结尾>>Foo.new.bar == "Hello World"
编辑:我最初写这个答案已经 9 年了,它值得做一些整容手术以保持最新.
您可以在此处查看编辑前的最新版本.
<小时>您不能通过名称或关键字调用 overwritten 方法.这就是为什么应该避免猴子补丁而首选继承的众多原因之一,因为显然您可以调用覆盖方法.
避免猴子补丁
继承
所以,如果可能的话,你应该更喜欢这样的:
class Foo定义栏'你好'结尾结尾类 ExtendedFoo <富定义栏超级 + '世界'结尾结尾ExtendedFoo.new.bar # =>'你好,世界'
如果您控制 Foo
对象的创建,则此方法有效.只需将创建 Foo
的每个位置更改为创建 ExtendedFoo
.如果您使用 依赖注入设计模式,工厂方法设计模式,抽象工厂设计模式或类似的东西,因为在这种情况下,你只需要改变地方.
委托
如果您不控制 Foo
对象的创建,例如因为它们是由您控制之外的框架创建的(例如 ruby-on-rails 例如),那么你可以使用包装设计模式:
需要'委托'Foo类定义栏'你好'结尾结尾类 WrappedFoo <委托类(Foo)定义初始化(wrapped_foo)极好的结尾定义栏超级 + '世界'结尾结尾foo = Foo.new # 这实际上不在你的代码中,它来自其他地方wrapped_foo = WrappedFoo.new(foo) # 这在你的控制之下wrapped_foo.bar # =>'你好,世界'
基本上,在系统边界,Foo
对象进入您的代码,您将它包装到另一个对象中,然后使用 那个 对象而不是代码中其他地方的原始代码.
这使用 Object#来自
标准库中的库.delegate
辅助方法>
干净"的猴子补丁
Module#prepend
: Mixin 前置
以上两种方法都需要更改系统以避免猴子补丁.本节展示了猴子补丁的首选和最小侵入性方法,如果不能选择更改系统.
Module#prepend
被添加以或多或少地支持这个用例.Module#prepend
与 Module#include
做同样的事情,除了它直接在类的下面混合混合:
class Foo定义栏'你好'结尾结尾模块 FooExtensions定义栏超级 + '世界'结尾结尾Foo类预先添加 FooExtensions结尾Foo.new.bar # =>'你好,世界'
注意:我也在这个问题中写了一些关于 Module#prepend
的内容:Ruby 模块前置与推导
Mixin 继承(损坏)
我看到有些人尝试(并询问为什么它在 StackOverflow 上不起作用)这样的事情,即 include
ing 一个 mixin 而不是 prepend
ing它:
class Foo定义栏'你好'结尾结尾模块 FooExtensions定义栏超级 + '世界'结尾结尾Foo类包括 FooExtensions结尾
不幸的是,这行不通.这是一个好主意,因为它使用了继承,这意味着您可以使用 super
.但是,Module#include
插入继承层次结构中类上方的 mixin,这意味着 FooExtensions#bar
永远不会被调用(如果它被调用,super
实际上不会引用 Foo#bar
而是引用不存在的 Object#bar
),因为 Foo#bar
总是最先被找到.
方法包装
最大的问题是:我们怎样才能坚持bar
方法,而不实际保留实际方法?答案在于函数式编程,正如它经常做的那样.我们将方法作为一个实际的对象来持有,并且我们使用一个闭包(即一个块)来确保我们并且只有我们持有该对象:
class Foo定义栏'你好'结尾结尾Foo类old_bar = instance_method(:bar)define_method(:bar) 做old_bar.bind(self).() + '世界'结尾结尾Foo.new.bar # =>'你好,世界'
这很干净:由于old_bar
只是一个局部变量,它会在类体的末尾超出范围,并且不可能从任何地方访问它,甚至 使用反射!由于 Module#define_method
需要一个块,并且块靠近它们周围的词法环境(这就是为什么我们在这里使用 define_method
而不是 def
),it(并且只有它)仍然可以访问old_bar
,即使它已经超出范围.
简短说明:
old_bar = instance_method(:bar)
这里我们将 bar
方法包装成一个 UnboundMethod
方法对象并将其分配给局部变量 old_bar
.这意味着,即使 bar
已被覆盖,我们现在也可以保留它.
old_bar.bind(self)
这有点棘手.基本上,在 Ruby(以及几乎所有基于单分派的面向对象语言)中,一个方法绑定到特定的接收器对象,在 Ruby 中称为 self
.换句话说:一个方法总是知道它被调用的是什么对象,它知道它的 self
是什么.但是,我们直接从一个类中获取方法,它怎么知道它的self
是什么?
好吧,它没有,这就是为什么我们需要bind
我们的 UnboundMethod
首先是一个对象,它会返回一个 Method
对象,我们可以调用它.(UnboundMethod
s 不能被调用,因为它们不知道自己的 self
不知道该做什么.)
我们将它绑定
到什么?我们只需将它绑定
到我们自己,这样它就会完全像原来的bar
那样表现!
最后,我们需要调用从 bind
返回的 Method
.在 Ruby 1.9 中,有一些漂亮的新语法 (.()
),但如果您使用的是 1.8,则可以简单地使用 call
方法;无论如何,这就是 .()
被翻译成的.
这里有几个其他问题,其中解释了其中一些概念:
脏"猴子补丁
alias_method
链
我们在猴子补丁中遇到的问题是,当我们覆盖该方法时,该方法就消失了,因此我们无法再调用它.所以,让我们做一个备份副本吧!
class Foo定义栏'你好'结尾结尾Foo类别名方法 :old_bar, :bar定义栏old_bar + '世界'结尾结尾Foo.new.bar # =>'你好,世界'Foo.new.old_bar # =>'你好'
问题在于,我们现在用一个多余的 old_bar
方法污染了命名空间.这个方法会出现在我们的文档中,它会出现在我们 IDE 的代码完成中,它会在反射期间出现.此外,它仍然可以调用,但大概是我们猴子修补了它,因为我们首先不喜欢它的行为,所以我们可能不希望其他人调用它.
尽管它有一些不受欢迎的特性,但不幸的是,它已经通过 AciveSupport 的 Module#alias_method_chain
.
旁白:改进
如果您只需要在几个特定位置而不是整个系统中的不同行为,您可以使用 Refinements 将猴子补丁限制在特定范围内.我将在这里使用上面的 Module#prepend
示例进行演示:
class Foo定义栏'你好'结尾结尾模块扩展Foo模块 FooExtensions定义栏超级 + '世界'结尾结尾精炼Foo预先添加 FooExtensions结尾结尾Foo.new.bar # =>'你好'# 我们还没有激活我们的 Refinement!使用 ExtendedFoo# 激活我们的细化Foo.new.bar # =>'你好,世界'#它来了!
您可以在此问题中看到使用 Refinements 的更复杂示例:如何为特定方法启用猴子补丁?一个>
<小时>废弃的想法
在 Ruby 社区确定 Module#prepend
之前,有多种不同的想法在流传,您可能偶尔会在较早的讨论中看到这些想法的引用.所有这些都包含在 Module#prepend
中.
方法组合器
一个想法是来自 CLOS 的方法组合器的想法.这基本上是面向方面编程子集的一个非常轻量级的版本.
使用类似的语法
class Foo定义栏:之前# 将始终在 bar 之前运行,当 bar 被调用时结尾定义栏:之后# 将始终在 bar 之后运行,当 bar 被调用时# 可能会也可能无法访问和/或更改 bar 的返回值结尾结尾
您将能够挂钩"bar
方法的执行.
然而,您是否以及如何在 bar:after
中访问 bar
的返回值还不是很清楚.也许我们可以(ab)使用 super
关键字?
class Foo定义栏'你好'结尾结尾Foo类定义栏:之后超级 + '世界'结尾结尾
更换
before 组合子等同于 prepend
使用一个覆盖方法在 mixin 中调用 super
在方法的末尾.同样,after 组合子相当于前置
使用覆盖方法super
在方法的开始调用mixin.>
你也可以在之前和调用super
之后做一些事情,你可以多次调用super
,同时检索和操作super
的返回值,使得 prepend
比方法组合器更强大.
class Foo定义栏:之前# 将始终在 bar 之前运行,当 bar 被调用时结尾结尾# 是相同的模块 BarBefore定义栏# 总是在 bar 之前运行,当 bar 被调用时极好的结尾结尾Foo类前置 BarBefore结尾
和
class Foo定义栏:之后# 将始终在 bar 之后运行,当 bar 被调用时# 可能会也可能无法访问和/或更改 bar 的返回值结尾结尾# 是相同的类 BarAfter定义栏original_return_value = 超级# 将始终在 bar 之后运行,当 bar 被调用时# 可以访问并且可以改变 bar 的返回值结尾结尾Foo类前置 BarAfter结尾
old
关键字
这个想法添加了一个类似于 super
的新关键字,它允许你调用 overwritten 方法,就像 super
允许你调用重写方法:
class Foo定义栏'你好'结尾结尾Foo类定义栏旧 + '世界'结尾结尾Foo.new.bar # =>'你好,世界'
这样做的主要问题是它向后不兼容:如果您有名为 old
的方法,您将无法再调用它!
更换
super
在 prepend
ed mixin 的覆盖方法中与本提案中的 old
基本相同.
redef
关键字
与上面类似,但不是为调用被覆盖的方法添加一个新关键字并单独保留def
,而是添加一个新关键字用于重新定义em> 方法.这是向后兼容的,因为目前的语法无论如何都是非法的:
class Foo定义栏'你好'结尾结尾Foo类重新定义栏旧 + '世界'结尾结尾Foo.new.bar # =>'你好,世界'
除了添加两个新关键字,我们还可以重新定义redef
中super
的含义:
class Foo定义栏'你好'结尾结尾Foo类重新定义栏超级 + '世界'结尾结尾Foo.new.bar # =>'你好,世界'
更换
redef
ining 一个方法相当于覆盖一个 prepend
ed mixin 中的方法.覆盖方法中的 super
在这个提案中的行为类似于 super
或 old
.
Say I am monkey patching a method in a class, how could I call the overridden method from the overriding method? I.e. Something a bit like super
E.g.
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
EDIT: It has been 9 years since I originally wrote this answer, and it deserves some cosmetic surgery to keep it current.
You can see the last version before the edit here.
You can’t call the overwritten method by name or keyword. That’s one of the many reasons why monkey patching should be avoided and inheritance be preferred instead, since obviously you can call the overridden method.
Avoiding Monkey Patching
Inheritance
So, if at all possible, you should prefer something like this:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
This works, if you control creation of the Foo
objects. Just change every place which creates a Foo
to instead create an ExtendedFoo
. This works even better if you use the Dependency Injection Design Pattern, the Factory Method Design Pattern, the Abstract Factory Design Pattern or something along those lines, because in that case, there is only place you need to change.
Delegation
If you do not control creation of the Foo
objects, for example because they are created by a framework that is outside of your control (like ruby-on-rails for example), then you could use the 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'
Basically, at the boundary of the system, where the Foo
object comes into your code, you wrap it into another object, and then use that object instead of the original one everywhere else in your code.
This uses the Object#DelegateClass
helper method from the delegate
library in the stdlib.
"Clean" Monkey Patching
Module#prepend
: Mixin Prepending
The two methods above require changing the system to avoid monkey patching. This section shows the preferred and least invasive method of monkey patching, should changing the system not be an option.
Module#prepend
was added to support more or less exactly this use case. Module#prepend
does the same thing as Module#include
, except it mixes in the mixin directly below the class:
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'
Note: I also wrote a little bit about Module#prepend
in this question: Ruby module prepend vs derivation
Mixin Inheritance (broken)
I have seen some people try (and ask about why it doesn’t work here on StackOverflow) something like this, i.e. include
ing a mixin instead of prepend
ing it:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
Unfortunately, that won’t work. It’s a good idea, because it uses inheritance, which means that you can use super
. However, Module#include
inserts the mixin above the class in the inheritance hierarchy, which means that FooExtensions#bar
will never be called (and if it were called, the super
would not actually refer to Foo#bar
but rather to Object#bar
which doesn’t exist), since Foo#bar
will always be found first.
Method Wrapping
The big question is: how can we hold on to the bar
method, without actually keeping around an actual method? The answer lies, as it does so often, in functional programming. We get a hold of the method as an actual object, and we use a closure (i.e. a block) to make sure that we and only we hold on to that 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'
This is very clean: since old_bar
is just a local variable, it will go out of scope at the end of the class body, and it is impossible to access it from anywhere, even using reflection! And since Module#define_method
takes a block, and blocks close over their surrounding lexical environment (which is why we are using define_method
instead of def
here), it (and only it) will still have access to old_bar
, even after it has gone out of scope.
Short explanation:
old_bar = instance_method(:bar)
Here we are wrapping the bar
method into an UnboundMethod
method object and assigning it to the local variable old_bar
. This means, we now have a way to hold on to bar
even after it has been overwritten.
old_bar.bind(self)
This is a bit tricky. Basically, in Ruby (and in pretty much all single-dispatch based OO languages), a method is bound to a specific receiver object, called self
in Ruby. In other words: a method always knows what object it was called on, it knows what its self
is. But, we grabbed the method directly from a class, how does it know what its self
is?
Well, it doesn’t, which is why we need to bind
our UnboundMethod
to an object first, which will return a Method
object that we can then call. (UnboundMethod
s cannot be called, because they don’t know what to do without knowing their self
.)
And what do we bind
it to? We simply bind
it to ourselves, that way it will behave exactly like the original bar
would have!
Lastly, we need to call the Method
that is returned from bind
. In Ruby 1.9, there is some nifty new syntax for that (.()
), but if you are on 1.8, you can simply use the call
method; that’s what .()
gets translated to anyway.
Here are a couple of other questions, where some of those concepts are explained:
"Dirty" Monkey Patching
alias_method
chain
The problem we are having with our monkey patching is that when we overwrite the method, the method is gone, so we cannot call it anymore. So, let’s just make a backup copy!
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'
The problem with this is that we have now polluted the namespace with a superfluous old_bar
method. This method will show up in our documentation, it will show up in code completion in our IDEs, it will show up during reflection. Also, it still can be called, but presumably we monkey patched it, because we didn’t like its behavior in the first place, so we might not want other people to call it.
Despite the fact that this has some undesirable properties, it has unfortunately become popularized through AciveSupport’s Module#alias_method_chain
.
An aside: Refinements
In case you only need the different behavior in a few specific places and not throughout the whole system, you can use Refinements to restrict the monkey patch to a specific scope. I am going to demonstrate it here using the Module#prepend
example from above:
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!
You can see a more sophisticated example of using Refinements in this question: How to enable monkey patch for specific method?
Abandoned ideas
Before the Ruby community settled on Module#prepend
, there were multiple different ideas floating around that you may occasionally see referenced in older discussions. All of these are subsumed by Module#prepend
.
Method Combinators
One idea was the idea of method combinators from CLOS. This is basically a very lightweight version of a subset of Aspect-Oriented Programming.
Using syntax like
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
you would be able to "hook into" the execution of the bar
method.
It is however not quite clear if and how you get access to bar
’s return value within bar:after
. Maybe we could (ab)use the super
keyword?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Replacement
The before combinator is equivalent to prepend
ing a mixin with an overriding method that calls super
at the very end of the method. Likewise, the after combinator is equivalent to prepend
ing a mixin with an overriding method that calls super
at the very beginning of the method.
You can also do stuff before and after calling super
, you can call super
multiple times, and both retrieve and manipulate super
’s return value, making prepend
more powerful than method combinators.
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
and
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
This idea adds a new keyword similar to super
, which allows you to call the overwritten method the same way super
lets you call the overridden method:
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
The main problem with this is that it is backwards incompatible: if you have method called old
, you will no longer be able to call it!
Replacement
super
in an overriding method in a prepend
ed mixin is essentially the same as old
in this proposal.
redef
keyword
Similar to above, but instead of adding a new keyword for calling the overwritten method and leaving def
alone, we add a new keyword for redefining methods. This is backwards compatible, since the syntax currently is illegal anyway:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Instead of adding two new keywords, we could also redefine the meaning of super
inside redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Replacement
redef
ining a method is equivalent to overriding the method in a prepend
ed mixin. super
in the overriding method behaves like super
or old
in this proposal.
这篇关于当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!