モンキーがインスタンスメソッドにパッチを適用するときに、オーバーライドされたメソッドを新しい実装から呼び出すことができますか?


442

クラスのメソッドにサルのパッチを適用しているとしましょう。オーバーライドするメソッドからオーバーライドされるメソッドを呼び出すにはどうすればよいですか?すなわち少し何かsuper

例えば

class Foo
  def bar()
    "Hello"
  end
end 

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

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

最初のFooクラスは他のクラスであり、2番目のFooはそれから継承する必要はありませんか?
Draco Ater

1
いいえ、私はサルのパッチです。私は、元のメソッドを呼び出すために使用することができます)スーパーのようなもの(があるだろう期待していた
ジェームズ・ホリングワース

1
これは、の作成Foo 使用を制御しない場合に必要ですFoo::bar。そのため、メソッドにモンキーパッチを適用する必要があります。
HalilÖzgür2014年

回答:


1165

編集:私が最初にこの回答を書いてから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ます。Dependency Injection Design PatternFactory Method Design PatternAbstract Factory Design Patternを使用すると、これはさらにうまく機能しますまたはそれらの線に沿ったものその場合、変更する必要がある場所だけがあるためです。

委任

の作成を制御しない場合、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オブジェクトがコードに入るシステムの境界で、オブジェクトを別のオブジェクトにラップし、コード内の他のすべての場所で元のオブジェクトの代わりにそのオブジェクトを使用ます。

これは、stdlibのライブラリObject#DelegateClassからヘルパーメソッドを使用しdelegateます。

「クリーン」なサルのパッチ

Module#prepend:Mixin追加

上記の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モジュールの先頭に追加するか派生するか

Mixin継承(破損)

私はつまり、何人かの人々がしよう(と、それはStackOverflowの上ここでは動作しない理由について尋ねる)このようなものを見てきましたincludeミックスインをINGの代わりにprependそれをINGの。

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

残念ながら、それはうまくいきません。継承を使用するため、これは良いアイデアですsuper。つまり、を使用できます。しかし、Module#includeミックスインを挿入上記手段継承階層内のクラス、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_methoddefold_bar

簡単な説明:

old_bar = instance_method(:bar)

ここでは、barメソッドをUnboundMethodメソッドオブジェクトにラップし、それをローカル変数に割り当てていますold_bar。これは、bar上書きされた後も保持する方法があることを意味します。

old_bar.bind(self)

これは少しトリッキーです。基本的に、Ruby(およびほとんどすべての単一ディスパッチベースのOO言語)では、selfRuby で呼び出される特定のレシーバーオブジェクトにメソッドがバインドされます。言い換えると、メソッドは、呼び出されたオブジェクトを常に認識しており、それが何であるかを認識してい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'

この問題は、不要な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!

この質問では、洗練を使用したより洗練された例を見ることができます:特定のメソッドでサルのパッチを有効にする方法は?


放棄されたアイデア

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キーワードを(ab)使用できますか?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

置換

前コンビネータは、と等価であるprepend呼び出すオーバーライドメソッドとミックスインをINGのsuper非常に終了方法の。同様に、コンビネータ後と同等ですprepend呼び出すオーバーライドメソッドでミックスインをINGの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、それを呼び出すことができなくなります!

置換

superprependedミックスインのオーバーライドメソッドの場合old、この提案と基本的に同じです。

redef キーワード

上記と同様です、上書きされたメソッドを呼び出すための新しいキーワードdefを追加してそのままにするのではなく、メソッドを再定義するための新しいキーワードを追加します。構文は現在いずれにせよ違法であるため、これは下位互換性があります。

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

2つの新しいキーワードを追加する代わりに、superinside の意味を再定義することもできますredef

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

置換

redefメソッドの初期化は、prependedミックスインでメソッドをオーバーライドすることと同じです。super以下のようなオーバーライドメソッドの振る舞いにおけるsuperまたはoldこの提案インチ


@JörgW Mittag、メソッドラッピングアプローチはスレッドセーフですか?2つの同時スレッドbindが同じold_method変数を呼び出すとどうなりますか?
Harish Shetty、2012

1
@KandadaBoggu:私はそれが何を意味するのか正確に理解しようとしています:-)しかし、Rubyの他の種類のメタプログラミングと同じくらいスレッドセーフであると私は確信しています。特に、へのすべての呼び出しUnboundMethod#bindは新しい別のを返すMethodため、異なるスレッドから2回続けて呼び出すか、2回同時に呼び出すかに関わらず、競合は発生しません。
イェルクWミッターク

1
RubyとRailsを使い始めて以来、このようなパッチの説明を探していました。正解です。私にとって欠けているのは、class_evalとクラスの再開についてのメモだけでした。ここにあります:stackoverflow.com/a/10304721/188462
Eugene


5
どこで見つけるのですかoldredef?私の2.0.0にはそれらがありません。ああ、それは逃さない難しいたルビーにそれをしなかった他の競合のアイデアを:
Nakilon


-1

オーバーライドを行うクラスは、元のメソッドを含むクラスの後にリロードする必要があります。そのため、オーバーライドを行うrequireファイル内でそれを行います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.