我眼中的元編程-方法篇

Ruby是一門動態語言,動態創建與調用方法是其中一個體現。

動態方法

動態調用方法(動態派發)

動態調用方法,是指在代碼中不通過硬編碼而是在程序運行時自動去決定要調用的方法的一種行為。

示例代碼1
class Student
   attr_accessor :name, :age, :birthday
   def initialize(args = {})
     name = args[:name]
     age    = args[:age]
     birthday = args[:birthday]
   end
end

【示例代碼1】initialize方法中,給三個字段賦值的方式就是一種典型的硬編碼方式,假如這三個字段的名稱有改動,抑或添加、去掉字段的時候,不得不同時修改這個方法。為了避免這種情況,這里可以考慮使用動態調用的方式來重構它。

示例代碼2
class Student
  attr_accessor :name, :age, :birthday
  def initialize(args = {}) 
    args.each do |key, value|
       method_name = "#{key}="
       self.send("#{key}=", value) if self.respond_to?(method_name)
    end
  end
end

通過【示例代碼2】的重構,但凡attr_accessor后面的字段有變動時,initialize方法都會自動進行適配。那么實現的原理是什么呢?

在Ruby中,方法調用其實是向一個對象發送了一條消息,當接收方接收消息后,會在對象的祖先鏈中去尋找這個方法,找到之后調用它并返回給self對象(詳細見【對象模型篇】)。也就是說,當調用str.method的時候,本質上就是發送了一條方法調用的消息,接收者是str對象,它等價于str.send(:method)。因此示例代碼便很好理解了,它是將args這個hash中的值進行遍歷,動態調用attr_accessor生成的settergetter方法。但有一個問題,如果參數中有在attr_accessor未定義的字段怎么辦?比如Student.new({ year: 2016 }),year字段是未在attr_accessor中定義的,如果調用self.year =這個方法,是會拋異常的。所以這里添加了respond_to?來判斷這個方法是否是存在的,存在再對它進行調用賦值。

動態定義方法

關于動態定義方法,其實在第一章對象模型篇【示例代碼1】已經在使用了,就是對define_method的使用。在此基礎之上,此處實現一個更加具有可用性的案例:

示例代碼3
module Kernel
   def attr_access(*args)
      args.each do |arg|
         define_method(arg) do
            instance_variable_get("@#{arg}")
         end
         define_method("#{arg}=") do |value|
            instance_variable_set("@{arg}=", value)
         end
      end
   end

  def cattr_access(*args)
     args.each do |arg|
        define_singleton_method(arg) do 
           self.class_variable_get("@@#{arg}")
        end
        define_singleton_method("#{arg}=") do |value|
           self.class_variable_set("@@#{arg}", value)
        end
     end
  end
end

class A
  cattr_access :a
end
A.a = 1
p A.a  # 輸出1

此處不再說明define_methodattr_access的使用,重點說明一下define_singleton_methodcattr_access的實現。

define_singleton_methoddefine_method的區別是,前者定義的是單例方法(這里可稱為類方法),后者定義的是實例方法。從用法來看,cattr_access聲明的變量直接在類(這里是A)上調用,而attr_access聲明的變量需要在A類對象實例化(A.new)之后調用。同理,class_variable_setclass_variable_get定義的是單例變量(這里指類變量),而instance_variable_setinstance_variable_get定義的是實例變量。由于Ruby的語法約定,以@開頭的為實例變量,以@@開頭的為類變量,因此,在定義變量時尤其要注意變量的全名,否則會拋異常。

幽靈方法

還記得之前在方法查找中,如果找不到方法時,會觸發一個NoMethodError的異常拋出。然而它來源于向對象發送了一個消息調用了一個方法叫做method_missing。

假如對一個String類對象str調用test_method_a,即str.test_method_a,由于這個方法未定義,因此在祖先鏈中找不到這個方法。此時會發送一個消息str.send(:method_missing, :test_method_a),從而拋出NoMethodError的異常。也就是說,當找不到要調用的方法時,會自動觸發調用method_missing方法。那么如果重寫了某個類的method_missing方法會是什么樣的結果呢?

示例代碼4
class XmlGen
  def method_missing(name, *args, &block)
     if %W(html head title body).include?(name.to_s)
        define_singleton_method(name) do |arg = nil, &blk|
           str  = "<#{name}>"
           str += arg if arg
           str += blk.call if blk
           str += "</#{name}>"
           str
        end
        self.send(name, *args, &block)
     end
  end
end

xml = XmlGen.new
str = xml.html do 
   xml.head do
      xml.title "Test"
   end
end
p str  # 輸出<html><head><title>Test</title></head></html>

由于在method_missing中對調用方法的名字做了限制,必須是html、headtitle、body其中之一才會生成代碼,因此無需擔心其它額外正常調用不存在方法的時候不能正常拋出NoMethodError異常的情況。由于在調用不存在的方法時就會調用method_missing這個方法,因此如果要重寫這個方法一定要格外小心,能力越大,責任越大。

幽靈方法與普通動態方法的優劣

普通動態方法是指,在類初始化時便使用define_method等手段將需要的所有方法定義好。幽靈方法本質是在調用時,如果發現不存在方法時,那么即時定義這個方法并產生一次調用,從示例可以看出幽靈方法在定義方法時也是調用的define_method等行為來定義動態方法。與普通定義動態方法的區別是,如果一個對象永遠沒有調用一個方法,那么這個方法永遠不會被定義,只有調用過一次時它才會被定義,因此使用幽靈方法時,對象所占用的內存空間比普通動態方法要少,反之付出的代價是第一次在祖先鏈中查找該方法的時間變長。這可以認為是一種以時間換取空間的策略。

動態代理

動態代理的原理是,對a對象的操作轉移到b對象上來,Ruby中使用delegate庫來實現動態代理。

示例代碼5
class UserProfile
    def initialize(name)
       @name = name
    end
    def hello
       "#{@name} says hello."
    end
end

class User < DelegateClass(UserProfile)
   def initialize(user_profile)
      super(user_profile)
   end
end
user_profile = UserProfile.new("Rapheal")
user = User.new(user_profile)
p user.hello  # 輸出 "Rapheal says hello."
關于respond_to?

respond_to?是Ruby中用于判斷一個方法是否存在的一個方法。比如,Class.respond_to?(:new) #返回true,說明Class這個類可以調用new方法。這個方法通常與define_method、method_missing等方法一起使用,與method_missing一樣,不到萬不得已,不要修改這個方法。

本章主要講了使用define_method來定義動態方法,使用method_missing來處理NoMethodError的情況。依然是那句話,能力越大,責任越大。如果能加以善用,那么這些特征能使的代碼的靈活度越來越高,反之只能使之晦澀難懂甚至導致難以追蹤的BUG。慎之!慎之!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,757評論 0 9
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,829評論 25 708
  • 傍晚的天空,陰沉沉的,好似要下雨。但,還是固執地換好運動裝出門了,去跑步。心中就一個信念,跑步去。終于,戰勝了懶惰...
    烏鴉一只閱讀 271評論 0 0
  • 為什么別人手機里放出來的歌,有時候就是覺得好聽呢?Beyond的《海闊天空》,在吃早餐的地方聽到的,覺得特別美好。...
    安擇閱讀 273評論 0 2