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,因為它的程式碼寫起來比較乾淨,也清楚許多。
Comments