Techioz Blog

Rubyでメソッドを一時的に再定義するにはどうすればよいですか?

概要

言ってください、私は次のものを持っています:

class Test
  def initialize(m)
    @m = m
  end

  def test
    @m
  end
end

Test のすべてのインスタンス (既存と新規の両方) のメソッド #test が一時的に 113 を返し、後で元のメソッドを復元するにはどうすればよいですか?

とても簡単なことのように思えますが、それを達成するための良い方法が見つかりません。私のRubyの知識が乏しいせいかもしれません。

私がこれまでに見つけたものは次のとおりです。

# saving the original method
Test.send(:alias_method, :old_test, :test)

# redefining the method
Test.send(:define_method, :test) { 113 }

# restore the original method
Test.send(:alias_method, :test, :old_test)

どちらが役割を果たしますが、私が理解しているように、既存の #old_test が存在する場合はそれも再定義することになります?.. そして、それはメタプログラミングの適切な使用というよりもハッキングのように感じます?.

たとえ難しいものや非現実的なものであっても、同じことを達成するための複数の方法を説明していただければ幸いです。 Ruby のメタプログラミングの柔軟性と制限についてのアイデアを提供するために:)

ありがとうございました🤗

追伸私がこれらすべてを始めた理由: gem ラックスロットルを使用して /api で始まるリクエストをスロットルしていますが、他の URL は影響を受けないはずです。これらすべてをテストして、それが機能することを確認したいと考えています。スロットルをテストするには、テスト環境にもミドルウェアを追加する必要がありました。 (minitest を使用して) 正常にテストできましたが、ApiController をテストする他のすべてのテストは調整すべきではありません。各リクエストの後に 1 秒待つ必要がある場合、テストにかかる時間が大幅に長くなるからです。

RequestSpecificIntervalThrottle#allowed? にモンキーパッチを適用することにしました。 minitest の #setups で { true } を使用して、これらすべてのテストのスロットルを一時的に無効にし、その後 #teardowns で再度有効にします (そうしないと、スロットル自体をテストするテストが失敗します)。これについてどのようにアプローチするかを教えていただければ幸いです。

しかし、すでにメタプログラミングについて掘り下げ始めているので、実際に使用するつもりはないとしても、これをどのように達成するか (メソッドを一時的に再定義する) にも興味があります。

解決策

instance_method を使用すると、任意のインスタンス メソッドから UnboundMethod オブジェクトを取得できます。

class Foo
  def bar
    "Hello"
  end
end
old_method = Foo.instance_method(:bar)
# Redifine the method
Foo.define_method(:bar) do
  puts "Goodbye"
end
puts Foo.new.bar # Goodbye

# restore the old method:
Foo.define_method(old_method.name, old_method)

非バインド メソッドはオブジェクト化された時点のメソッドへの参照であり、基になるクラスに対するその後の変更は非バインド メソッドには影響しません。

クラスメソッドに相当するものは次のとおりです。

class Foo
  def self.baz
    "Hello"
  end
end

old_method = Foo.method(:baz).unbind

世界最小の (そしておそらく最も役に立たない) スタブ ライブラリを作成したい場合は、次のように実行できます。

class Stubby
  def initialize(klass, method_name, &block)
    @klass = klass
    @old_method = klass.instance_method(method_name)
    @klass.define_method(method_name, &block)
  end

  def restore
    @klass.define_method(@old_method.name, @old_method)
  end

  def run_and_restore
    yield
  ensure
    restore
  end
end

puts Foo.new.bar # Hello

Stubby.new(Foo, :bar) do
  "Goodbye"
end.run_and_restore do
  puts Foo.new.bar # Goodbye
end

puts Foo.new.bar # Hello