web-dev-qa-db-ja.com

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

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

例えば。

class Foo
  def bar()
    "Hello"
  end
end 

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

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

EDIT:私が最初にこの答えを書いてから9年が経ちましたが、最新の状態に保つためには美容整形手術に値します。

編集前の最後のバージョンを見ることができます here


名前またはキーワードでoverwrittenメソッドを呼び出すことはできません。これは、明らかにcanoverriddenメソッドを呼び出すため、モンキーパッチを回避し、代わりに継承を優先する多くの理由の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オブジェクトの作成を制御する場合、たとえば、オブジェクトが制御外のフレームワークによって作成されるため(たとえば Ruby-on-Rails など) 、次に 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オブジェクトがコードに含まれるシステムの境界で、それを別のオブジェクトにラップし、コード内の元々のオブジェクトの代わりにthatオブジェクトを使用します。

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

「クリーン」モンキーパッチ

Module#prepend :Mixin Prepending

上記の2つの方法では、サルのパッチを回避するためにシステムを変更する必要があります。このセクションでは、システムの変更がオプションではない場合の、サルのパッチングの優先的かつ最も侵襲性の低い方法を示します。

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'

注:この質問でModule#prependについても少し書きました: Ruby module prepend vs derivation

ミックスイン継承(破損)

私は何人かの人々がこれを試して(そしてStackOverflowでなぜここで動作しないのかを尋ねる)、すなわちincludeingの代わりにprependingをミックスインするのを見ました:

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は呼び出されません(そしてwereが呼び出された場合、 superは実際にはFoo#barを参照するのではなく、むしろObject#barが常に最初に見つかるため、存在しないFoo#barを参照します。

メソッドのラッピング

大きな問題は、実際に実際のメソッドを保持せずに、どのようにbarメソッドを保持するかということです。答えは、関数型プログラミングによくあるように、あります。メソッドを実際のobjectとして保持し、クロージャー(つまりブロック)を使用して、and we onlyそのオブジェクトを保持していることを確認します。

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は単なるローカル変数であるため、クラス本体の最後でスコープ外になり、どこからでもアクセスすることはできませんeven反射を使用して!そして Module#define_method はブロックを取り、ブロックは周囲の字句環境を閉じます(これはwhyここではdefの代わりにdefine_methodを使用しています)、it =(およびonly it)は、スコープ外になった後でも、old_barにアクセスできます。

簡単な説明:

old_bar = instance_method(:bar)

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

old_bar.bind(self)

これは少し注意が必要です。基本的に、Ruby(およびほぼすべてのシングルディスパッチベースのOO言語)では、メソッドはRubyでselfと呼ばれる特定のレシーバーオブジェクトにバインドされます。言い換えれば、メソッドは常にどのオブジェクトが呼び出されたかを知っており、selfが何であるかを知っています。しかし、クラスから直接メソッドを取得しましたが、そのselfが何であるかをどのように知るのでしょうか?

そうではありません。そのため、まずオブジェクトに bindUnboundMethodを指定する必要があります。このオブジェクトは、後で呼び出すことができる Method オブジェクトを返します。 (UnboundMethodsは、selfを知らなければ何をすべきかわからないため、呼び出すことができません。)

そして、bindそれに何をしますか?私たちは単にbindを自分自身に振る舞います。そのように振る舞いますexactly元の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:after内でbarの戻り値にアクセスできるかどうか、およびどのようにアクセスするかは明確ではありません。 superキーワードを(ab)使用できますか?

class Foo
  def bar
    'Hello'
  end
end 

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

置換

Beforeコンビネータは、メソッドの非常にendprependを呼び出すオーバーライドメソッドでミックスインをsuperingすることと同等です。同様に、afterコンビネーターは、メソッドの非常にbeginningprependを呼び出すオーバーライドメソッドを使用して、ミックスインをsuperingすることと同等です。

また、superを呼び出した後、andの前に何かを行うことができます。また、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に似た新しいキーワードを追加します。これにより、superoverriddenメソッドを呼び出すのと同じ方法でoverwrittenメソッドを呼び出すことができます。

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

これの主な問題は、後方互換性がないことです:oldと呼ばれるメソッドがある場合、それを呼び出すことができなくなります!

置換

superedミックスインのオーバーライドメソッドのprependは、この提案のoldと本質的に同じです。

redefキーワード

上記と同様ですが、上書きされるメソッドcallingに新しいキーワードを追加し、defをそのままにする代わりに、redefiningメソッドに新しいキーワードを追加します。いずれにせよ、構文は現在不正であるため、これは後方互換性があります。

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

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

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

置換

メソッドをredefiningすることは、prependedミックスインでメソッドをオーバーライドすることと同等です。オーバーライドメソッドのsuperは、この提案のsuperまたはoldのように動作します。

1099
Jörg W Mittag

エイリアスメソッドを見てください。これは、メソッドの名前を新しい名前に変更するようなものです。

詳細と開始点については、これをご覧ください メソッドの置き換え記事 (特に最初の部分)。 Ruby API docs も、(より複雑ではない)例を提供します。

12
Veger