堆疊修飾(Stackable modification)


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

就語法上來看,除了類別可以繼承或具有多個特徵,但只能繼承一個抽象類別之外,特徵與抽象類別間的不同點,就是特徵不能定義主要建構式的參數,還有就是特徵中定義方法時,super呼叫是動態綁定的(類別中的super是靜態綁定的,也就是你在類別中寫下super.somMethod()時,你知道所呼叫的是哪個類別的方法)。

特 徵中定義方法時,super呼叫是動態綁定的,也就是當你在特徵的方法中寫下super.someMethod()時,你並不知道真正呼叫的是哪個類別的 方法,只有在某個類別具有該特徵時,才能決定super.someMethod()所呼叫的是哪個類別的方法,也由於這個特性,在類別具有這類特徵時,得以在呼 叫super.someMethod()方法前後作些處理,執行所謂堆疊修飾(Stackable modification)的功能。

舉個例子來說,你打算設計一個點餐程式,目前主餐有炸雞、漢堡,你打算讓點了主餐的客入選擇附餐時可以有優惠,如果您使用繼承的方式來達到這個目的,例如:
class FriedChicken {
    def content = "不黑心炸雞"
    def price = 49.0
}

class SideDishOne extends FriedChicken {
    override def content = super.content + " | 可樂 | 薯條"
    override def price = super.price + 30.0
}


這個需求直接使用繼承並不適當,你繼承父類別之後,只是取得父類別的price結果再進一步加以處理,另一方面,如果漢堡也想要搭配附餐一,目前的SideDishOne顯然無法給漢堡重用,你還得為漢堡建立有附餐一的子類別。

為了讓附餐設計可以重複使用,你可以將附餐的行為特徵抽出:
trait Meal {
def content: String
def price: Double
}

trait SideDishOne extends Meal { // 附餐一
abstract override def content = super.content + " | 可樂 | 薯條"
abstract override def price = super.price + 30.0
}

trait SideDishTwo extends Meal { // 附餐二
abstract override def content = super.content + " | 玉米濃湯 | 咖啡"
abstract override def price = super.price + 50.0
}

class FriedChicken extends Meal {
def content = "不黑心炸雞"
def price = 49.0
}

附 餐也是一種餐點,所以抽出Meal特徵規範共同的介面content與price,而附餐一與附餐二重新定義了content與price,也就是原來特 徵定義是抽象方法,現有定義為有實作內容的方法,不過由於其中用到了super呼叫,目前並無法知道這個呼叫到底是呼叫哪個方法,因此加上了 abstract關鍵字。

現在如何搭配附餐呢?為了詳細示範,先中規中矩地寫的話,可以這麼用:
class FriedChickenSideDishOne extends FriedChicken with SideDishOne
val meal = new FriedChickenSideDishOne
println(meal.content) // 顯示 不黑心炸雞 | 可樂 | 薯條
println(meal.price) // 顯示 79.0

你的類別繼承FriedChicken類別並具有SideDishOne特徵,FriedChickenSideDishOne類別中,SideDishOne特徵的content 與price方法重新定義了FriedChicken類別中content與price方法,你呼叫meal.content時,根據特徵中的定義,就是 先呼叫父類別FriedChicken的content取得結果,再附加上字串後傳回,呼叫meal.price時也是同樣的道理。

實際上,你可以這麼使用:
val meal1 = new FriedChicken with SideDishOne
println(meal1.content) // 顯示 不黑心炸雞 | 可樂 | 薯條
println(meal1.price) // 顯示 79.0

第一行其實是匿名類別的寫法,你打算建立一個物件,該物件實例化自一個類別繼承自FriedChicken類別並具有SideDishOne特徵,不過你沒有定義出類別的名稱。

你可以隨意地組合套餐:
val meal1 = new FriedChicken with SideDishOne
val meal2 = new FriedChicken with SideDishTwo
val meal3 = new FriedChicken with SideDishOne with SideDishTwo
val meal4 = new FriedChicken with SideDishTwo with SideDishOne
println(meal1.content) // 不黑心炸雞 | 可樂 | 薯條
println(meal1.price) // 79.0
println(meal2.content) // 不黑心炸雞 | 玉米濃湯 | 咖啡
println(meal2.price) // 99.0
println(meal3.content) // 不黑心炸雞 | 可樂 | 薯條 | 玉米濃湯 | 咖啡
println(meal3.price) // 129.0
println(meal4.content) // 不黑心炸雞 | 玉米濃湯 | 咖啡 | 可樂 | 薯條
println(meal4.price) // 129.0

注 意meal3與meal4,這也是為什麼這個特性被稱為堆疊修飾的原因,你讓物件具有兩個以上的特徵時,最右邊的特徵若使用super.content呼叫,其 實是在呼叫左邊特徵的content方法,而左邊特徵的super.content則呼叫父類別的content,呼叫的結果就像是將最左邊的類別看作是 堆疊底部,最右邊的特徵看作是堆疊頂端。

同時也注意到,meal3是先繼承SideDishOne特徵再具有SideDishTwo,而meal4則是先繼承SideDishTwo再具有SideDishOne,具有特徵的順序不同,則呼叫的順序不同,則結果就有所不同。

事實上,這是Scala版本的 Decorator 模式 之實現,雖然語法不同,不過其在不改變被修飾物件功能的情況下,動態地為物件的操作結果作修飾,這樣的精神是相同的。