3

クライアントのカスタマイズを追加するためのコアクラスにモジュールを含むアプリがあります。

class_evalは、コアクラスのメソッドをオーバーライドするのに適した方法であることがわかりましたが、メソッド全体を書き直さずに、元のメソッドだけを使用したい場合があります。

たとえば、というメソッドがある場合account_balance、モジュール(つまり、クラスに含まれるモジュール)で次のようなことを行うと便利です。

module CustomClient
  def self.included base
    base.class_eval do
      def account_balance
        send_alert_email if balance < min
        super # Then this would just defer the rest of the logic defined in the original class
      end
    end
  end
end

しかし、class_evalを使用superすると、ルックアップパスからメソッドが削除されるようです。

誰かがこれを回避する方法を知っていますか?

ありがとう!

4

4 に答える 4

12

あなたがやりたいことをする方法はいくつかあると思います。1つは、クラスを開き、古い実装のエイリアスを作成することです。

class MyClass
  def method1
    1
  end
end

class MyClass
  alias_method :old_method1, :method1
  def method1
    old_method1 + 1
  end
end

MyClass.new.method1
 => 2 

これはモンキーパッチの一種なので、適度にイディオムを使用するのがおそらく最善です。また、共通の機能を保持する別のヘルパーメソッドが必要な場合もあります。

編集:より包括的なオプションのセットについては、JörgWMittagの回答を参照してください。

于 2012-12-10T18:18:15.357 に答える
9

instance_evalは、コアクラスのメソッドをオーバーライドするための良い方法であることがわかりました。

あなたはオーバーライドしていません。あなたは別名モンキーパッチを上書きしています。

ただし、メソッド全体を書き直さずに、元のメソッドだけを使用したい場合があります。

元の方法に従うことはできません。独自の方法はありません。あなたはそれを上書きしました。

しかし、instance_evalを使用superすると、ルックアップパスからメソッドが削除されるようです。

あなたの例には継承はありません。super場に出ることさえありません。

考えられる解決策と代替案については、この回答を参照してください。モンキーパッチを適用するときに、新しい実装からオーバーライドされたメソッドを呼び出すことができますか?

于 2012-12-10T19:30:41.190 に答える
1

あなたが言うように、alias_methodは注意深く使用されなければなりません。この不自然な例を考えると:

module CustomClient
...    
    host.class_eval do
      alias :old_account_balance :account_balance
      def account_balance ...
        old_account_balance
      end
...
class CoreClass
    def old_account_balance ... defined here or in a superclass or
                                in another included module
    def account_balance
        # some new stuff ...
        old_account_balance # some old stuff ...
    end
    include CustomClient
end

エイリアスの後、old_account_balanceはaccount_balanceのコピーであり、これは自分自身を呼び出すため、無限ループになります。

$ ruby -w t4.rb 
t4.rb:21: warning: method redefined; discarding old old_account_balance
t4.rb:2: warning: previous definition of old_account_balance was here
[ output of puts removed ]
t4.rb:6: stack level too deep (SystemStackError)

[つるはしから]このテクニック[alias_method]の問題は、old_xxxという既存のメソッドがないことに依存していることです。より良い代替策は、事実上匿名であるメソッドオブジェクトを利用することです。

そうは言っても、ソースコードを所有している場合は、単純なエイリアスで十分です。しかし、より一般的なケースでは、Jörgのメソッドラッピング手法を使用します。

class CoreClass
    def account_balance
        puts 'CoreClass#account_balance, stuff deferred to the original method.'
    end
end

module CustomClient
  def self.included host
    @is_defined_account_balance = host.new.respond_to? :account_balance
    puts "is_defined_account_balance=#{@is_defined_account_balance}"
        # pass this flag from CustomClient to host :
    host.instance_variable_set(:@is_defined_account_balance,
                                @is_defined_account_balance)
    host.class_eval do
      old_account_balance = instance_method(:account_balance) if
                @is_defined_account_balance
      define_method(:account_balance) do |*args|
        puts 'CustomClient#account_balance, additional stuff'
            # like super :
        old_account_balance.bind(self).call(*args) if
                self.class.instance_variable_get(:@is_defined_account_balance)
      end
    end
  end
end

class CoreClass
    include CustomClient
end

print 'CoreClass.new.account_balance : '
CoreClass.new.account_balance

出力:

$ ruby -w t5.rb 
is_defined_account_balance=true
CoreClass.new.account_balance : CustomClient#account_balance, additional stuff
CoreClass#account_balance, stuff deferred to the original method.

クラス変数@@is_defined_account_balanceはどうですか?[つるはしから]インクルードを含むモジュールまたはクラス定義は、インクルードするモジュールの定数、クラス変数、およびインスタンスメソッドにアクセスできます。
CustomClientからホストに渡すことを避け、テストを簡素化します。

    old_account_balance if @@is_defined_account_balance # = super

しかし、グローバル変数と同じくらいクラス変数を嫌う人もいます。

于 2012-12-11T12:18:25.490 に答える
0

[つるはしから]メソッドObject#instance_evalを使用すると、selfを任意のオブジェクトに設定し、でブロック内のコードを評価してから、selfをリセットできます。

module CustomClient
  def self.included base
    base.instance_eval do
      puts "about to def account_balance in #{self}"
      def account_balance
        super
      end
    end
  end
end

class Client
    include CustomClient #=> about to def account_balance in Client
end

ご覧のとおりdef account_balance、モジュールを含むホストクラスであるClientクラスのコンテキストで評価されるため、account_balanceはClientのシングルトンメソッド(別名クラスメソッド)になります。

print 'Client.singleton_methods : '
p Client.singleton_methods #=> Client.singleton_methods : [:account_balance]

Client.new.account_balanceはインスタンスメソッドではないため、機能しません。

「コアクラスにモジュールを含むアプリがあります」

詳細はあまり説明していませんが、次のインフラストラクチャを想像しました。

class SuperClient
    def account_balance
        puts 'SuperClient#account_balance'
    end
end

class Client < SuperClient
    include CustomClient
end

次に、instance_evalをclass_evalに置き換えます。[Pickaxeから]class_evalは、クラス定義の本体にいるかのように設定するため、メソッド定義はインスタンスメソッドを定義します。

module CustomClient
...
   base.class_eval do
...

print 'Client.new.account_balance : '
Client.new.account_balance

出力:

  #=> from include CustomClient :
about to def account_balance in Client #=> as class Client, in the body of Client
Client.singleton_methods : []
Client.new.account_balance : SuperClient#account_balance #=> from super


"But using instance_eval seems to take the super method out of the lookup path."

super働いています。問題はinstance_evalでした。

于 2012-12-10T21:39:05.760 に答える