クラス内のメソッドにモンキーパッチを適用するとします。オーバーライドしたメソッドからオーバーライドされたメソッドを呼び出すにはどうすればよいでしょうか? つまり、次のようなものです。super
例えば
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
ベストアンサー1
編集: この回答を最初に書いてから 9 年が経ちました。最新の状態に保つために整形手術を行う価値があります。
編集前の最終バージョンを見ることができますここ。
上書きされたメソッドを名前またはキーワードで呼び出すことはできません。これは、モンキー パッチを避けて継承を優先するべき多くの理由の 1 つです。オーバーライドされたメソッドを呼び出すことは明らかに可能です。
モンキーパッチングの回避
継承
したがって、可能であれば、次のようなものが望ましいでしょう。
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
これは、Foo
オブジェクトの作成を制御する場合に機能します。 を作成するすべての場所をFoo
を作成するように変更するだけですExtendedFoo
。依存性注入設計パターン、ファクトリーメソッド設計パターン、抽象ファクトリー設計パターンまたはそれに類するもので、その場合、変更する必要があるのは 1 か所だけだからです。
委任
オブジェクトの作成を制御できない場合Foo
、例えば、オブジェクトが制御外のフレームワークによって作成される場合(ルビーオンレール例えば、ラッパーデザインパターン:
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
stdlib 内のライブラリ。
「クリーンな」モンキーパッチング
Module#prepend
: ミックスインの先頭追加
上記の 2 つの方法では、モンキー パッチングを回避するためにシステムを変更する必要があります。このセクションでは、システムの変更が選択肢にない場合に推奨される、最も侵襲性の低いモンキー パッチングの方法を示します。
Module#prepend
は、ほぼこのユースケースをサポートするために追加されました。Module#prepend
は、 と同じことを行いますが、クラスの直下Module#include
にミックスインをミックスする点が異なります。
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 と 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
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
モンキーパッチングで発生する問題は、メソッドを上書きするとメソッドが消えてしまい、呼び出せなくなることです。そこで、バックアップ コピーを作成しましょう。
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 を使用してモンキー パッチを特定の範囲に制限できます。ここでは、上記の例を使用してこれを説明します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 を使用したより洗練された例を見ることができます。特定のメソッドに対してモンキーパッチを有効にするにはどうすればいいですか?
放棄されたアイデア
Ruby コミュニティが に落ち着くまでModule#prepend
、さまざまなアイデアが出回っていました。これらは、古い議論でときどき参照されていることがあります。これらはすべて に含まれますModule#prepend
。
メソッドコンビネータ
1 つのアイデアは、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
。 キーワードを (悪用) できるでしょうかsuper
?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
交換
before コンビネータは、メソッドの最後に呼び出すオーバーライド メソッドを持つミックスインと同等です。同様に、after コンビネータは、メソッドのprepend
最初に呼び出すオーバーライドメソッドを持つミックスインと同等です。super
prepend
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
、それを呼び出すことができなくなります。
交換
super
ed ミックスイン内のオーバーライドメソッドは、この提案prepend
と本質的に同じです。old
redef
キーワード
上記と似ていますが、上書きされたメソッドを呼び出してそのままにしておくための新しいキーワードを追加する代わりに、メソッドを再定義するdef
ための新しいキーワードを追加します。構文は現在とにかく違法であるため、これは下位互換性があります。
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
2 つの新しいキーワードを追加する代わりに、 super
insideの意味を再定義することもできますredef
。
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
交換
redef
メソッドを in することは、prepend
ed ミックスインでメソッドをオーバーライドすることと同じです。super
オーバーライド メソッドでは、この提案のsuper
または のように動作しますold
。