程式扎記: [Scala 小學堂] Scala Gossic : 了解更多 - 混入特徵 (堆疊修飾)

標籤

2016年8月9日 星期二

[Scala 小學堂] Scala Gossic : 了解更多 - 混入特徵 (堆疊修飾)

轉載自 這裡 
前言 : 
Scala 是一個可直譯、編譯、靜態、運行於 JVM 之上、可與 Java 互操作、融合物件導向編程特性與函式編程風格的程式語言. 特徵抽離共同的介面與實作,類別動態地繼承(extends)或具有(with)特徵. 

堆疊修飾 (Stackable modification) : 
特徵(Trait)可以讓你定義沒有實作的抽象方法,也可以讓你定義有實作的具體方法,事實上,抽象類別可以作的事,特徵幾乎都可以作(特徵也可以定義資料成員以持有狀態),你可能會想,特徵跟抽象類別有何不同? 

就語法上來看,除了類別可以繼承或具有多個特徵,但只能繼承一個抽象類別之外,特徵與抽象類別間的不同點,就是特徵不能定義主要建構式的參數,還有就是特徵中定義方法時,super 呼叫是動態綁定的類別中的 super 是靜態綁定的,也就是你在類別中寫下super.somMethod() 時,你知道所呼叫的是哪個類別的方法). 特 徵中定義方法時,super 呼叫是動態綁定的,也就是當你在特徵的方法中寫下 super.someMethod() 時,你並不知道真正呼叫的是哪個類別的 方法,只有在某個類別具有該特徵時,才能決定super.someMethod() 所呼叫的是哪個類別的方法,也由於這個特性,在類別具有這類特徵時,得以在呼 叫 super.someMethod() 方法前後作些處理,執行所謂堆疊修飾Stackable modification)的功能. 舉個例子來說,你打算設計一個點餐程式,目前主餐有炸雞、漢堡,你打算讓點了主餐的客入選擇附餐時可以有優惠,如果您使用繼承的方式來達到這個目的,例如 : 
  1. class FriedChicken {  
  2.     def content = "不黑心炸雞"  
  3.     def price = 49.0  
  4. }  
  5.   
  6. class SideDishOne extends FriedChicken {  
  7.     override def content = super.content + " | 可樂 | 薯條"  
  8.     override def price = super.price + 30.0  
  9. }  
這個需求直接使用繼承並不適當,你繼承父類別之後,只是取得父類別的 price 結果再進一步加以處理,另一方面,如果漢堡也想要搭配附餐一,目前的 SideDishOne 顯然無法給漢堡重用,你還得為漢堡建立有附餐一的子類別. 為了讓附餐設計可以重複使用,你可以將附餐的行為特徵抽出 : 
- FriedChicken.scala 代碼 :
  1. trait Meal {  
  2.     def content: String  
  3.     def price: Double  
  4. }  
  5.   
  6. trait SideDishOne extends Meal { // 附餐一  
  7.     abstract override def content = super.content + " | 可樂 | 薯條"  
  8.     abstract override def price = super.price + 30.0  
  9. }  
  10.   
  11. trait SideDishTwo extends Meal {  // 附餐二  
  12.     abstract override def content = super.content + " | 玉米濃湯 | 咖啡"  
  13.     abstract override def price = super.price + 50.0  
  14. }  
  15.   
  16. class FriedChicken extends Meal {  
  17.     def content = "不黑心炸雞"  
  18.     def price = 49.0  
  19. }  

附餐 (SideDish) 也是一種餐點,所以抽出 Meal 特徵 規範共同的介面 content 與 price,而附餐一與附餐二重新定義了content 與 price,也就是原來特徵定義是抽象方法,現有定義為有實作內容的方法,不過由於其中用到了 super 呼叫,目前並無法知道這個呼叫到底是呼叫哪個方法,因此加上了 abstract 關鍵字. 現在如何搭配附餐呢?為了詳細示範,先中規中矩地寫的話,可以這麼用 : 
  1. class FriedChickenSideDishOne extends FriedChicken with SideDishOne  
  2. val meal = new FriedChickenSideDishOne  
  3. println(meal.content)   // 顯示 不黑心炸雞 | 可樂 | 薯條  
  4. println(meal.price)     // 顯示 79.0  
你的類別繼承 FriedChicken 類別並具有 SideDishOne 特徵,FriedChickenSideDishOne 類別中,SideDishOne 特徵的 content 與 price 方法重新定義了 FriedChicken 類別中 content 與 price 方法,你呼叫 meal.content 時,根據特徵中的定義,就是 先呼叫父類別 FriedChicken 的 content 取得結果,再附加上字串後傳回,呼叫 meal.price 時也是同樣的道理. 實際上,你可以這麼使用 : 
  1. val meal1 = new FriedChicken with SideDishOne  
  2. println(meal1.content)    // 顯示 不黑心炸雞 | 可樂 | 薯條  
  3. println(meal1.price)      // 顯示 79.0  
第一行其實是匿名類別的寫法,你打算建立一個物件,該物件實例化自一個類別繼承自 FriedChicken 類別並具有 SideDishOne 特徵,不過你沒有定義出類別的名稱. 你可以隨意地組合套餐 : 
  1. val meal1 = new FriedChicken with SideDishOne  
  2. val meal2 = new FriedChicken with SideDishTwo  
  3. val meal3 = new FriedChicken with SideDishOne with SideDishTwo  
  4. val meal4 = new FriedChicken with SideDishTwo with SideDishOne  
  5. println(meal1.content)   // 不黑心炸雞 | 可樂 | 薯條  
  6. println(meal1.price)     // 79.0  
  7. println(meal2.content)   // 不黑心炸雞 | 玉米濃湯 | 咖啡  
  8. println(meal2.price)     // 99.0  
  9. println(meal3.content)   // 不黑心炸雞 | 可樂 | 薯條 | 玉米濃湯 | 咖啡  
  10. println(meal3.price)     // 129.0  
  11. println(meal4.content)   // 不黑心炸雞 | 玉米濃湯 | 咖啡 | 可樂 | 薯條  
  12. println(meal4.price)     // 129.0  
注 意 meal3 與 meal4,這也是為什麼這個特性被稱為堆疊修飾的原因,你讓物件具有兩個以上的特徵時,最右邊的特徵若使用 super.content 呼叫,其實是在呼叫左邊特徵的 content 方法,而左邊特徵的 super.content 則呼叫父類別的 content,呼叫的結果就像是將最左邊的類別看作是 堆疊底部,最右邊的特徵看作是堆疊頂端. 同時也注意到,meal3 是先繼承 SideDishOne 特徵再具有 SideDishTwo,而 meal4 則是先繼承 SideDishTwo 再具有 SideDishOne,具有特徵的順序不同,則呼叫的順序不同,則結果就有所不同. 事實上,這是 Scala 版本的 Decorator 模式 之實現,雖然語法不同,不過其在不改變被修飾物件功能的情況下,動態地為物件的操作結果作修飾,這樣的精神是相同的. 

Supplement : 
[OO 設計模式] Decorator Pattern : 裝飾模式 - 動態增加類別功能

沒有留言:

張貼留言

網誌存檔