当猴子修补一个方法时,你可以从新的实现中调用重写的方法吗?
说我是猴子补丁在类中的方法,我怎么能从重写的方法调用重写的方法? 即有点像super
例如
class Foo def bar() "Hello" end end class Foo def bar() super() + " World" end end >> Foo.new.bar == "Hello World"
编辑 :从我最初写这个答案已经5年了,它值得一些整容手术,以保持当前。
在编辑之前,您可以看到最后一个版本。
您不能通过名称或关键字调用覆盖的方法。 这就是为什么应该避免使用猴子修补和inheritance优先的原因之一,因为显然你可以调用重写的方法。
避免猴子补丁
遗产
所以,如果可能的话,你应该喜欢这样的东西:
class Foo def bar 'Hello' end end class ExtendedFoo < Foo def bar super + ' World' end end ExtendedFoo.new.bar # => 'Hello World'
这个工作,如果你控制Foo
对象的创build。 只要改变创buildFoo
每个地方,而不是创build一个ExtendedFoo
。 如果你使用dependency injectiondevise模式 , 工厂方法devise模式 , 抽象工厂devise模式或者其他方法 ,那么这样做会更好,因为在这种情况下,只有你需要改变的地方。
代表团
如果您不控制Foo
对象的创build,例如因为它们是由您的控制之外的框架(例如ruby-on-rails )创build的,那么您可以使用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
对象进入你的代码,你把它包装到另一个对象,然后使用该对象,而不是原来的代码中的其他任何地方。
这使用stdlib中delegate
库中的Object#DelegateClass
帮助器方法。
“清洁”猴子补丁
Module#prepend
:Mixin Prepending
上述两种方法需要改变系统以避免猴子修补。 这部分显示了猴子补丁的首选和最less侵入的方法,应该改变系统不是一个选项。
Module#prepend
,以支持或多或less这个用例。 Module#prepend
与Module#include
的function相同,除了混合在类的正下方的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模块prepend与派生
Mixininheritance(破坏)
我曾经见过一些人尝试(并询问为什么它不能在StackOverflow中工作),就像这样, include
一个mixin而不是prepend
:
class Foo def bar 'Hello' end end module FooExtensions def bar super + ' World' end end class Foo include FooExtensions end
不幸的是,这是行不通的。 这是一个好主意,因为它使用inheritance,这意味着你可以使用super
。 但是, Module#include
在inheritance层次结构的类的上面插入了mixin,这意味着FooExtensions#bar
永远不会被调用(如果被调用, super
将不会实际引用Foo#bar
而是引用Object#bar
不存在),因为Foo#bar
始终会被首先find。
方法包装
最大的问题是:我们怎样才能坚持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
只是一个局部variables,它将在类体的末尾超出范围, 即使使用reflection也无法从任何地方访问它! 而且,由于Module#define_method
占用了一个块,并且阻塞了它们周围的词法环境(这就是为什么我们在这里使用define_method
而不是def
)的原因, 它 ( 只有它)仍然可以访问old_bar
,即使它已经出去范围。
简短的解释:
old_bar = instance_method(:bar)
在这里,我们将bar
方法封装到UnboundMethod
方法对象中,并将其分配给本地variablesold_bar
。 这意味着,我们现在有一种方法可以在被覆盖之后继续保持bar
。
old_bar.bind(self)
这有点棘手。 基本上,在Ruby(几乎所有基于单个调度的OO语言)中,方法都绑定到一个特定的接收者对象,在Ruby中称为self
。 换句话说:一个方法总是知道它被调用的是什么对象,它知道self
是什么。 但是,我们直接从课堂上抓了这个方法,它怎么知道self
是什么?
嗯,这不是,这就是为什么我们需要先bind
我们的UnboundMethod
到一个对象,然后返回一个我们可以调用的Method
对象。 ( UnboundMethod
不能被调用,因为他们不知道自己该怎么做。)
我们把它bind
到什么地方? 我们简单地把它bind
在自己身上,这样它就会像原来的bar
一样!
最后,我们需要调用从bind
返回的Method
。 在Ruby 1.9中,这个( .()
)有一些漂亮的新语法,但是如果你使用1.8,你可以简单地使用call
方法。 这就是.()
无论如何翻译。
以下是其他一些问题,其中解释了其中一些概念:
- 我如何在Ruby中引用一个函数?
- Ruby的代码块是否和C的lambdaexpression式相同?
“肮脏的”猴子补丁
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中,它会在reflection过程中出现。 而且,它还是可以被调用的,但是大概我们用猴子来修补它,因为我们不喜欢它的行为,所以我们可能不希望别人叫它。
尽pipe这有一些不良的特性,但不幸的是它已经通过AciveSupport的Module#alias_method_chain
变得stream行起来。
一边: 修饰
如果您只需要某些特定位置而不是整个系统中的不同行为,则可以使用“优化”将猴子修补程序限制到特定范围。 我将在这里使用上面的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
内的回报价值,并不十分清楚bar:after
。 也许我们可以(ab)使用super
关键字?
class Foo def bar 'Hello' end end class Foo def bar:after super + ' World' end end
替代
之前的combinator相当于在方法的最后调用super
方法的prepend
方法。 同样,后组合器相当于在方法一开始就用一个super
方法来调用super
方法。
你也可以在调用super
之前和之后做一些事情,你可以多次调用super
,并且检索和操作super
的返回值,使得比方法combinator更加强大。
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
方法,你将不能再调用它!
替代
super
在一个prepend
混合中的压倒一切的方法在本提案中基本上与old
的相同。
redef
关键字
与上面类似,不是添加一个新的关键字来调用被覆盖的方法,而是保留def
,我们添加一个新的关键字来重新定义方法。 这是向后兼容的,因为目前的语法是非法的:
class Foo def bar 'Hello' end end class Foo redef bar old + ' World' end end Foo.new.bar # => 'Hello World'
而不是增加两个新的关键字,我们也可以重新定义里面的redef
的super
redef
:
class Foo def bar 'Hello' end end class Foo redef bar super + ' World' end end Foo.new.bar # => 'Hello World'
替代
redef
一个方法就等同于在一个prepend
redef
中重写该方法。 super
方法在这个提议中performance得如同super
或者old
一样。
看一下别名方法,这是一种将方法重命名为新名称的方法。
有关更多信息和出发点,请参阅此replace方法文章 (特别是第一部分)。 Ruby API文档也提供了(不太复杂的)示例。
要覆盖的类必须在包含原始方法的类之后重新加载,所以require
它在覆盖的文件中。