程式扎記: [ Ruby Gossip ] Basic : 模組 - 堆疊擴充

標籤

2014年11月17日 星期一

[ Ruby Gossip ] Basic : 模組 - 堆疊擴充

Source From Here 
Preface 
如果你為物件定義單例方法,實際上該方法定義,會存在於物件的匿名單例類別中(Anonymous singleton class),你可以使用 class << object 語法,開啟 object 的單例類別。例如: 
  1. o = Object.new  
  2. def o.some  
  3.     puts "some"  
  4. end  
實際上也可以如下定義: 
  1. o = Object.new  
  2. class << o  
  3.     def some  
  4.         puts "some"  
  5.     end  
  6. end  
堆疊擴充 
這兩種方式的效果幾乎是相等的,目前就暫且看作是相等。你可以使用 class << object 語法,開啟 object 的單例類別,也就可以在其中作任何類別可以進行的定義,包括了 include 模組。例如臨時為某個物件增加 Enumerable 模組的作用: 
  1. class Pond  
  2.     def initialize(list = [])  
  3.         @list = list  
  4.     end  
  5.     def <<(obj)  
  6.         @list << obj  
  7.     end  
  8. end  
  9.   
  10. pond = Pond.new([215364])  
  11. class << pond  
  12.     include Enumerable  
  13.     def each  
  14.         @list.each do |obj|  
  15.             yield(obj)  
  16.         end  
  17.     end  
  18. end  
  19.   
  20. print "#{pond.sort}\n"   # [123456]  
  21. puts "Max: #{pond.max}"  # Max: 6  
  22. puts "Min: #{pond.min}"  # Min: 1  
  23.   
  24. pond.each_with_index do |ball, index|  
  25.     puts "#{index} - #{ball}"  
  26. end  
就語法上來看,模組與類別的不同點,就是模組無法實例化,還有類別只能繼承一個父類別,但可以 include 多個模組。 

模組中定義方法時,self 與 super 是動態綁定的,被包括到某個類別時,模組定義方法中的 self 代表該類別的實例,而 super 代表模組被 include 前同名的模組方法或類別方法如果是類別繼承,super 代表父類別中的同名方法)。 

由於模組中的 super 是動態綁定,而一個類別可以 include 多個模組,因此可執行所謂 堆疊擴充Stackable extension)的功能。 

舉個例子來說,你打算設計一個點餐程式,目前主餐有炸雞、漢堡,你打算讓點了主餐的客入選擇附餐時可以有優惠,如果使用繼承的方式來達到這個目的,例如: 
  1. class FriedChicken  
  2.     def content  
  3.         "不黑心炸雞"  
  4.     end  
  5.     def price  
  6.         49.0  
  7.     end  
  8. end  
  9.   
  10. class SideDishOne < FriedChicken  
  11.     def content  
  12.         super + " | 可樂 | 薯條"  
  13.     end  
  14.     def price  
  15.         super + 30.0  
  16.     end  
  17. end  
這個需求直接使用繼承並不適當,你繼承父類別之後,只是取得父類別的 price 結果再進一步加以處理,另一方面,如果漢堡也想要搭配附餐一,目前的 SideDishOne 顯然無法給漢堡重用,你還得為漢堡建立有附餐一的子類別。為了讓附餐設計可以重複使用,你可以將附餐的行為抽出為模組: 
  1. module SideDishOne  
  2.     def content  
  3.         super + " | 可樂 | 薯條"  
  4.     end  
  5.     def price  
  6.         super + 30.0  
  7.     end  
  8. end  
  9.   
  10. module SideDishTwo  
  11.     def content  
  12.         super + " | 玉米濃湯 | 咖啡"  
  13.     end  
  14.     def price  
  15.         super + 50.0  
  16.     end  
  17. end  
  18.   
  19. class FriedChicken  
  20.     def content  
  21.         "不黑心炸雞"  
  22.     end  
  23.     def price  
  24.        49.0  
  25.     end  
  26. end  
附餐一與附餐二都定義了 content 與 price,不過由於其中用到了 super 呼叫,目前無法知道這個呼叫到底是呼叫哪個方法。為了詳細示範搭配附餐,先中規中矩地寫的話,可以這麼用: 
  1. class FriedChickenSideDishOne < FriedChicken  
  2.     include SideDishOne  
  3. end  
  4.   
  5. meal = FriedChickenSideDishOne.new  
  6. puts meal.content  # 顯示 不黑心炸雞 | 可樂 | 薯條  
  7. puts meal.price    # 顯示 79.0  
你的 FriedChickenSideDishOne 類別繼承 FriedChicken 類別並 include 了 SideDishOne 模組,FriedChickenSideDishOne 類別中,SideDishOne 模組的 content 與 price 方法重新定義了 FriedChicken 類別中 content 與 price 方法,你呼叫 meal.content 時,根據模組中的定義,就是 先呼叫類別 FriedChicken 的 content 取得結果,再附加上字串後傳回,呼叫 meal.price 時也是同樣的道理。 

特別定義出 FriedChickenSideDishOne 類別看來奇怪,實際上,你可以這麼使用: 
  1. meal = FriedChicken.new  
  2.   
  3. class << meal  
  4.     include SideDishOne  
  5. end  
  6.   
  7. puts meal.content  # 顯示 不黑心炸雞 | 可樂 | 薯條  
  8. puts meal.price    # 顯示 79.0  
除了開啟單例類別來 include 模組的方式之外,物件有個 extend 方法,可以執行相同的作用。例如: 
  1. meal = FriedChicken.new.extend(SideDishOne)  
  2. puts meal.content  # 顯示 不黑心炸雞 | 可樂 | 薯條  
  3. puts meal.price    # 顯示 79.0  
有了 extend 方法,你可以隨意地組合套餐: 
  1. meal1 = FriedChicken.new.extend(SideDishOne)  
  2. meal2 = FriedChicken.new.extend(SideDishTwo)  
  3. meal3 = FriedChicken.new.extend(SideDishOne).extend(SideDishTwo)  
  4. meal4 = FriedChicken.new.extend(SideDishTwo).extend(SideDishOne)  
  5. puts meal1.content   # 不黑心炸雞 | 可樂 | 薯條  
  6. puts meal1.price     # 79.0  
  7. puts meal2.content   # 不黑心炸雞 | 玉米濃湯 | 咖啡  
  8. puts meal2.price     # 99.0  
  9. puts meal3.content   # 不黑心炸雞 | 可樂 | 薯條 | 玉米濃湯 | 咖啡  
  10. puts meal3.price     # 129.0  
  11. puts meal4.content   # 不黑心炸雞 | 玉米濃湯 | 咖啡 | 可樂 | 薯條  
  12. puts meal4.price     # 129.0  
注意 meal3 與 meal4,這也是為什麼這個特性被稱為 堆疊擴充 的原因,你讓物件擴充以上的模組時,最右邊的模組若在 content 中使用 super 呼叫,其 實是在呼叫左邊模組的 content 方法,而左邊模組的 content 中的 super 則呼叫父類別的 content,呼叫的結果就像是將最左邊的類別看作是 堆疊底部,最右邊的模組看作是堆疊頂端。 

同時也注意到,meal3 是先擴充 SideDishOne 模組再擴充 SideDishTwo,而 meal4 則是先擴充 SideDishTwo 再擴充 SideDishOne,擴充模組的順序不同,則呼叫的順序不同,則結果就有所不同。事實上,這是 Ruby 版本的 Decorator 模式 之實現,雖然語法不同,不過其在不改變被修飾物件功能的情況下,動態地為物件的操作結果作修飾,這樣的精神是相同的。

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!