Chapter 3: Methods - Part III

還記得在 Chapter 2 提過的 “method lookup” 嗎?是否有想過當不存在的方法被呼叫時,Ruby 的方法查詢流程會變得怎麼樣呢?今天就要來介紹 Ruby 在 BasicObject 內建的一個方法 - Method Missing 

Method Missing

Method missing 是 BasicObject 內建方法,可讓你攔截找不到方法時或是無法回應物件時的ㄧ 種安全機制。 我們知道在 Ruby 中所有類別都是繼承自 BasicObject,因此每一個類別都擁有 method_missing() 方法。這個方法存在的目的是當 Ruby 找不到呼叫的方法或是無法回應當物件時,就會呼叫 method_missing() 方法。透過方法複寫來重新定義 method_missing(),可以攔截原有的 method_missing() 方法 ,達到精簡程式碼的效果。

以下面程式碼為例:

class Animal
  def jump
    p "jumping"
  end
end

dog = Animal.new
dog.jump           # => jumping

我建立了一個 Animal 類別,裡面有 jump() 實體方法,當 dog 物件呼叫 jump() 方法,回傳 “jumping” 就是正確的值。

但如果我們叫 dog 物件說話呢?

dog.speak      # => undefined method `speak' for ... (NoMethodError)

很明顯地 dog 物件並沒有 speak() 方法,所以回傳 (NoMethodError) 其實是正確的錯誤訊息。 你也可以利用開放類別的技巧,覆寫 method_missing()方法,讓回傳的錯誤訊息更加獨特。

根據 Ruby 官方手冊中的 method_missing 方法:

method_missing(symbol [, *args] ) 

symbol: 這裡符號代表的是方法名稱,是必須要有的參數。 *args: 任何其他傳進來的引數

現在來試試覆寫 method_missing()

class Animal
  def jump
    p "jumping"
  end
  
def method_missing(method_name, *args)
    # 因為傳進來的是符號,比對之前要先轉換成字串
    if method_name.to_s == 'speak'
      p "我不會說話"
    else
      # 如果比對失敗,回傳原本的錯誤訊息 (NoMethodError)
      super
    end
  end
end

dog = Animal.new
dog.jump           # => "jumping"
dog.speak          # => "我不會說話"
dog.talk           # => (NoMethodError)
  • jump() 方法被呼叫時會遵循ㄧ般的 method lookup 流程,在 Animal 類別找到該方法。
  • speak() 方法被呼叫時,會找不到該方法,所以回到我們覆寫 method_missing 方法,並且在比對成功後,回傳 “我不會說話”。
  • talk() 方法被呼叫時,會找不到該方法,所以回到我們覆寫method_missing 方法,並且在比對失敗後,super 會呼叫跟 dog物件血緣最近的父類別的 method_missing()方法,一直延著繼承關係往上找,但是一直都沒找到,所以回傳原本的錯誤訊息 (NoMethodError)。

Ghost Methods

當你有需要定義很多類似的方法時,可以有目的地把這些方法整合起來讓 method_missing() 去處理。這有點像是跟物件說:"如果有人問你這個方法,你不懂的話就去做這件事"

對傳送者來說,其實有沒有經由 method_missing() 方法所得到訊息,並無太大差別,因為也是在物件上找到的方法,再回傳訊息而已。但是對接收者而言,從 method_missing() 方法傳出來的訊息是經過確認,沒有存在相對應的方法時才發出的。

物件無法回應或是不存在的方法就稱為 Ghost Methods,而有目的地去呼叫這些 Ghost methods,再經由 method_missing() 所動態產生的對應手法就稱為是 Dynamic proxies

Method Missing In Rails

相信寫過 rails 的朋友們對 find_by 方法都很熟悉,但其實 ActiveRecord 還提供了另一種寫法就是:

YourModel.find_by_AttributeName("value")
User.find_by_id(1)  # => return User with id equal to 1
User.find_by_id(0)  # => return nil if User cannot be found

你也可以將 id 換成其他表格內的屬性,例如:名字、年齡、等…

User.find_by_name("kevin")
User.find_by_age(16)
...

這種 find_by_AttributeName的寫法其實就使用的 method_missing 方法的技巧。

ActiveRecord 會去找 find_by 字尾的屬性,然後與資料庫的欄位做比對,如果比對成功,就回傳該筆資料。這就與先前在 Animal 類別內覆寫 method_missing() 方法雷同,只有當比對失敗時,會跑 super 繼承關係流程,最終 nil 值 或是 錯誤訊息才會出現。