Chapter 5: Class Definitions - Part II

What is Refinement ?

還記得 Open Classes 可能會造成的問題嗎?當我們利用開放類別的特性,覆寫了類別內原有的方法,就會造成了全域性的改變。這代表改寫方法之後,任何物件呼叫該方法都受到影響。

class String
  def length
    size > 5 ? "long" : "short"
  end
end

"War and Peace".length     # => "long"
"War".length               # => "short"

不同於 Monkey patch,Ruby 黑魔法 Refinements 所能影響的範圍是區域性,而不是全域性的。 自從 Ruby 2.0之後,你可以使用 refine 以更安全的方式覆寫方法。 用法是先定義一個模組,並且在該模組內裡面放你要覆寫的類別及方法。 當需要 refinements 的時候,並不會自動啟用,你必須以關鍵字 using 才能開始使用。

module StringExtensions
  refine String do
    def length
      size > 5 ? "long" : "short"
    end
  end
end

using StringExtensions
p "War and Peace".length # => "long"
p "War".length # => "short"

Refinements 與 Monkey patches 類似,但影響範圍不是全域性的。Refinements 的作用域是有限的,且只能活在這兩個地方 : (1) 自己本身的 refine block itself, 

(2) 從你開始呼叫 using 的程式碼為起點,一直到模組結束。那如果你是在最頂層呼叫 using 的話,就是一直到該檔案結束為止。

接著看以下範例:

class MyClass 
  def my_method
    "original my_method()"
  end

  def another_method 
    my_method
  end 
end

module MyClassRefinement 
  refine MyClass do
    def my_method
      "refined my_method()"
    end 
  end
end

self    # => "main", at the top level

using MyClassRefinement
MyClass.new.my_method      # => "refined my_method()"
MyClass.new.another_method # => "original my_method()"

你大概不會很驚訝當你看到 MyClass.new.my_method() 方法的回傳值是 “ refined my_method() “,但是為什麼呼叫 another_method() 的回傳值竟然是 “original my_method()” 呢?

明明我們使用 using 是在 another_method() 之前啊!!!原因是在 MyClass 類別中, another_method()方法內呼叫 my_method() 方法是在 using 之前,所以 Ruby 會呼叫原始、未經修改過版本的my_method() 方法。

以上這樣的結果其實是違反一般直覺的反應,因此在使用 Refinement 時,最好重複檢查呼叫方法的順序流程,並且記得 Refinement 只能用在一般模組上,即使類別是繼承自模組,也不能用在類別上。

Refinement Wrapper

現在我們學到了 Refinement 如何利用 using 關鍵字,來控制覆寫方法可影響的範圍。 接著介紹 Refinement 的另一項特性將可以取代 Around Aliases  -  super。 看看以下例子:

class MyClass
  def my_method(change = false)
    "original my_method()"
  end
  def another_method
    my_method
  end
end

module MyClassRefinement
  refine MyClass do
    def my_method(change)
      change ? super : "refined my_method()"
    end
  end
end

using MyClassRefinement
self                          # main

MyClass.new.my_method(true)   # => "original my_method()"

MyClass.new.my_method(false)  # => "refined my_method()"

MyClass.new.another_method    # => "original my_method()"

如果將 super 關鍵字放在重新定義後的方法內,就可以利用 super 取得原始舊版的方法。 再來一個範例:

module StringRefinement 
  refine String do
    def length
      super > 5 ? 'long' : 'short'
    end 
  end
end

using StringRefinement
"War and Peace".length # => "long"

上面的程式碼利用 refine 關鍵字,重新定義 String 類別並且打包ㄧ個 length() 方法。就如同其他的 Refinements, Refinement Wrapper 也只能活到該檔案結束為止。 就因為 Refinements 所能影響的範圍是區域性,所以通常被視為比 Around Alias 更安全的選項。

Prepended Wrapper

還記得在 Chapter 2 所介紹的 Module#prepend() 方法嗎?Include() 與 prepend() 類似,都可以將方法引入 module,其中的差別在於 prepend 所引入的方法會在該類別的下方。 就因為這個區別,讓模組內的方法可以被覆寫並且呼叫 super 來取得未經修改的原始方法。

module ExplicitString 
  def length
    super > 5 ? "long" : "short" 
  end
end

String.class_eval do 
  prepend ExplicitString
end

"War and Peace".length          # => "long"

在以上程式碼,我們在 ExplicitString 模組內重新定義了 length() 方法,在以 class_eval() 方法打開 String 類別,並且以 prepend 該模組在 String 類別的下方。 最後的結果就是,之後任何物件呼叫 String#length() 都會得到修改後的回傳值。 

小結

目前為止我們介紹了三種 method wrappers:

(1) Around Alias

(2) Refinement Wrapper

(3) Prepend Wrapper

其中我比較偏好的是 Refinement Wrapper ,因為它提供了其他 Wrappers 都沒有的特性就是可用關鍵字 using 來決定覆寫方法可影響的範圍。然而,此特性的罩門就是容易搞會模組內呼叫方法的順序,因而得到與預想不同的回傳值。書中推薦的 method wrapper 則是 Prepend Wrapper,因為它的程式碼寫起來比較乾淨,也清楚許多。