2015-10-15 64 views
-2

我有一些代碼是這樣的:使用define_method和元編程在Ruby中動態定義實例方法?

class Country 
    attr_reader :name 

    def initialize 
    @name  = "MyName".freeze 
    end 

    def government 
    @government ||= Government.new(self) 
    end 

    def symbols 
    @symbols ||= Symbols.new(self) 
    end 

    def economy 
    @economy ||= Economy.new(self) 
    end 

    def education 
    @education ||= Education.new(self) 
    end 

    def healthcare 
    @healthcare ||= Healthcare.new(self) 
    end 

    def holidays 
    @holidays ||= Holidays.new(self) 
    end 

    def religion 
    @religion ||= Religion.new(self) 
    end 

end 

我怎麼能動態創建的方法呢?我想:

class Country 
    attr_reader :name 

    COMPONENETS = %w(government symbols economy education healthcare holidays religion) 


    COMPONENETS.each do |m| 
    define_method(m) do |argument| 
     instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self)) 
    end 
    end 

    def initialize 
    @name  = "MyName".freeze 
    end 

end 

如果我嘗試:

puts Country.new.education.inspect

我得到以下錯誤:

country.rb:16:in `block (2 levels) in <class:Country>': wrong number of arguments (0 for 1) (ArgumentError) 
    from country.rb:27:in `<main>' 

缺少什麼我在這裏?

+1

爲什麼你會認爲你錯過了一些東西?是否有錯誤信息?如果是,那是什麼?輸出是不是你所期望的?如果是,輸出是什麼,輸出是什麼所期望的行爲?行爲不是你所期望的行爲嗎?如果是,行爲是什麼,你期望的行爲是什麼?沒有發生什麼事情?發生的事情不應該發生?是不是應該發生的事情?請提供一個最小的自包含測試用例,所述測試用例的輸入和預期輸出以及所有錯誤和警告的行爲。 –

+0

我想我錯過了某些東西因爲我沒有得到預期的行爲我只是更新了錯誤我得到的任何線索 –

+0

似乎是'self'這裏與'Country'類相關,而不是Country的實例,我應該如何解決? –

回答

2

在你的原代碼,您定義的所有方法不接受參數:

def education 
#   ^^^ 
    @education ||= Education.new(self) 
end 

在元程序代碼中,您定義了所有的方法以獲取單個參數cal導致argument

define_method(m) do |argument| 
#     ^^^^^^^^^^ 
    instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self)) 
end 

但是,零個參數來調用它:

puts Country.new.education.inspect 
#      ^^^ 

很顯然,你的方法是爲了偷懶干將,所以他們應該採取任何參數:

define_method(m) do 
    instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self)) 
end 

請注意,您的代碼還存在其他問題。在原始代碼中,如果實例變量未定義,則使用條件賦值僅執行賦值,nilfalse,而在元程序代碼中,您總是無條件地設置它。它應該是更多的東西是這樣的:

define_method(m) do 
    if instance_variable_defined?(:"@#{m}") 
    instance_variable_get(:"@#{m}") 
    else 
    instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self)) 
    end 
end 

注:我也刪除從呼叫Object.const_get使用正常的恆定查找規則來查找常數(即第一個詞彙向外然後向上在繼承層次) ,因爲這對應於你如何查找原始代碼片段中的常量。

這不完全等同於您的代碼,因爲它只在未定義時設置實例變量,也不在falsenil中設置實例變量,但我認爲這更接近您的意圖。

我想封裝這個代碼,以使其更清晰的意圖:

class Module 
    def lazy_attr_reader(name, default=(no_default = true), &block) 
    define_method(name) do 
     if instance_variable_defined?(:"@#{name}") 
     instance_variable_get(:"@#{name}") 
     else 
     instance_variable_set(:"@#{name}", 
      if no_default then block.(name) else default end) 
     end 
    end 
    end 
end 

class Country 
    attr_reader :name 

    COMPONENTS = %w(government symbols economy education healthcare holidays religion) 

    COMPONENTS.each do |m| 
    lazy_attr_reader(m) do |name| 
     const_get(name.capitalize).new(self)) 
    end 
    end 

    def initialize 
    @name = 'MyName'.freeze 
    end 
end 

這樣的話,別人讀你的Country類不會去「咦,所以這個循環定義方法有時得到,有時設置實例變量「,但反而認爲」啊,這是一個循環,創建懶惰的getter!「

1

我猜你不需要argument

class Country 
    attr_reader :name 

    COMPONENETS = %w(government symbols economy education healthcare holidays religion) 


    COMPONENETS.each do |m| 
    define_method(m) do 
     instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self)) 
    end 
    end 

    def initialize 
    @name  = "MyName".freeze 
    end 

end 
1

你可以簡單的使用eval:

class Country 
    attr_reader :name 

    COMPONENETS = %w(government symbols economy education healthcare holidays religion) 

    COMPONENETS.each do |m| 
    eval <<-DEFINE_METHOD 
    def #{m} 
    @#{m} ||= #{m.capitalize}.new(self) 
    end 
DEFINE_METHOD 
    end 

    def initialize 
    @name = "MyName".freeze 
    end 
end 
+0

哇,太棒了。謝謝@Jean Bob –