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

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

クラス内のメソッドにモンキーパッチを適用するとします。オーバーライドしたメソッドからオーバーライドされたメソッドを呼び出すにはどうすればよいでしょうか? つまり、次のようなものです。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ヘルパーメソッドからdelegatestdlib 内のライブラリ。

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

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 でなぜうまくいかないのかを質問している人) を見たことがあります。つまり、includemixin を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. (UnboundMethods 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最初に呼び出すオーバーライドメソッドを持つミックスインと同等です。superprependsuper

また、を呼び出す前後に操作を実行したりsuper、 を複数回呼び出したり、の戻り値superを取得および操作したりできるため、メソッド コンビネータよりも強力になります。superprepend

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、それを呼び出すことができなくなります。

交換

supered ミックスイン内のオーバーライドメソッドは、この提案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 つの新しいキーワードを追加する代わりに、 superinsideの意味を再定義することもできますredef

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

交換

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

おすすめ記事