2012-12-10 27 views
2

我有一個應用程序,其中包括模塊到核心類添加客戶端自定義。與class_eval一起使用超級

我發現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方法。

有誰知道如何解決這個問題?

謝謝!

回答

10

我認爲有幾種方法可以做你想做的事情。一是打開類和別名舊的實現:

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 

這是monkey patching一種形式,所以可能最好使用適量的成語。另外,有時需要的是一個獨立的幫助方法,它具有通用功能。

編輯:請參閱JörgW Mittag的答案以獲取更全面的選項。

+1

Rails有一個叫做'alias_method_chain'的方法,可以幫助解決這個問題。 –

+0

不錯。迂腐的問題:如果在混合進班級的模塊中完成了猴子補丁嗎? – Nathan

+0

@Nathan:我不認爲「猴子修補」有一個確切的定義。在Ruby中,存在元編程,這只是正常的做事方式(比如在C++中使用模板),然後是極端的元編程形式,它們變得聰明或積極,並以不明確的方式修改已經定義的事物。如果你太聰明,那可能是壞的補丁。如果你只是想解決問題,就像你的情況一樣,這可能是可以接受的猴子補丁。例如,Rails經常使用'alias_method_chain'。 –

8

我發現instance_eval的是覆蓋在覈心類方法的好辦法,

您還沒有覆蓋。你是覆蓋 aka monkeypatching。

但有時我想避免重寫整個方法,只是按照原來的方法。

您不能推遲到原來的方法。沒有原始的方法。你把它覆蓋了。

但是使用instance_eval似乎將super方法帶出查找路徑。

在你的例子中沒有繼承。 super甚至沒有發揮作用。

看到這個答案可能的解決方案和備選方案:When monkey patching a method, can you call the overridden method from the new implementation?

+0

我剛剛在上面的鏈接中添加了對您的答案的評論。我認爲alias_method可以用來爲超級提供一個合理的替代方案。感謝您指出Ruby 2.0地平線上的Module#prepend方法 - 非常好! – Nathan

0

[從鎬該方法的對象#instance_eval的,您可以設置自會有一些任意對象,評估與塊中的代碼,然後重置自。

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在類的客戶端,主機類型,其包括模塊的上下文中計算的,因此account_balance成爲客戶端的單方法(又名類方法):

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。

+0

呃,我的壞。當我寫這個時,我的意思是class_eval。我會嘗試編輯,以便清楚。我對這種混亂表示歉意。謝謝你的收穫。不,我根本沒有繼承,所以它不像你所描述的那樣工作。我使用'alias_method'來實現它的工作方式。 – Nathan

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? [來自Pickaxe]包含include的模塊或類定義可以訪問它包含的模塊的常量,類變量和實例方法。
這將避免從一個CustomClient傳遞到主機和簡化測試:

old_account_balance if @@is_defined_account_balance # = super 

但有些厭惡類變量之多全局變量。

+0

現在我遇到的問題是,一旦類被覆蓋(即使有條件地描述),下一個請求會在生產中使用被覆蓋的類。換句話說,該類發生了變異,需要重新加載,但在生產中,重新加載類是ab資源命中。 – Nathan

+0

@Nathan:1)類沒有被覆蓋,舊的方法是別名,並定義了新的方法2)我不明白「生產」和「重新加載」。 Ruby是動態的,變化在飛行中發生。 – BernardK

+0

我忘記提到這最終將用於Rails項目,所以當我提到生產時,我指的是Rails運行的生產模式 - 類在應用程序實例中加載一次。加載後,除非重新加載原始類(即重新啓動應用程序或配置其他相對昂貴的選項來重新加載類),否則對類的任何更改都會一直存在。 – Nathan

相關問題