1. ホーム
  2. ruby

[解決済み] インスタンスメソッドをモンキーパッチする場合、新しい実装からオーバーライドされたメソッドを呼び出すことは可能ですか?

2022-03-20 05:51:58

質問

クラス内のメソッドをモンキーパッチする場合、オーバーライドされたメソッドからオーバーライドされたメソッドを呼び出すにはどうすればよいでしょうか?すなわち、次のようなものです。 super

class Foo
  def bar()
    "Hello"
  end
end 

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

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

解決方法は?

EDIT : この回答を最初に書いてから9年が経ちましたので、最新の内容にするために美容整形手術を受ける価値があります。

編集前のバージョンはこちらでご覧いただけます。 こちら .


を呼び出すことはできません。 上書きされた メソッドを名前かキーワードで指定します。これは、モンキーパッチが避けられ、代わりに継承が好まれる多くの理由のうちの一つです。 できる を呼び出します。 オーバーライド メソッドを使用します。

モンキーパッチを回避する

継承

だから、なるべくなら、こんな感じのものがいい。

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

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

の生成を制御している場合、これはうまくいきます。 Foo オブジェクトを作成します。を作成するすべての場所を変更するだけです。 Foo を作成し、代わりに ExtendedFoo . この機能は 依存性注入デザインパターン は、その ファクトリーメソッドデザインパターン 抽象ファクトリーデザインパターン というのも、その場合、変更する必要があるのは一箇所だけだからです。

デレゲーション

もし、あなたが しない の作成を制御します。 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 ただし、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とderivation

ミキシンの継承(壊)

このようなことを試している人を見たことがあります (そして 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 はミキシンを挿入します。 上記 は、継承階層にあるクラスです。 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_method ではなく def ここで)。 それ (そして だけ へのアクセスは可能です。 old_bar スコープ外に出た後でも。

簡単に説明します。

old_bar = instance_method(:bar)

ここでは bar メソッドを UnboundMethod メソッドオブジェクトを作成し、それをローカル変数 old_bar . これはつまり、私たちは今、次のような方法があることを意味します。 bar が上書きされた後でも

old_bar.bind(self)

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

さて、そうではないのですが、そのために必要なのが bind 我々の UnboundMethod をオブジェクトに変換し、そのオブジェクトが Method オブジェクトを呼び出すことができます。( UnboundMethod を知らないと何をすればいいのかわからないので、呼び出すことはできません。 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!

Refinementsを使ったより洗練された例を、この質問で見ることができます。 特定のメソッドに対してモンキーパッチを有効にするには?


断念したアイデア

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 . もしかしたら super というキーワードで検索してみてください。

class Foo
  def bar
    'Hello'
  end
end 

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

交換

ビフォアコンビネーターは、次のものと同等です。 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 のオーバーライドメソッドで prepend ed mixin は、基本的に old を提案します。

redef キーワード

上記と似ていますが、新しいキーワードを追加する代わりに 呼び出し を残し、上書きされたメソッドを def を単独で使用する場合、新しいキーワードを 再定義 メソッドになります。これは後方互換性があり、現在の構文はいずれにせよ違法だからです。

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

を追加する代わりに の意味を再定義することもできます。 super 内側 redef :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

交換

redef の中でメソッドをオーバーライドするのと同じです。 prepend というミキシンがあります。 super のような挙動をします。 super または old を提案します。