Decorator
December 30, 2021你打算設計一個點餐程式,目前主餐有炸雞、漢堡,而套餐組合可以有優惠,如果使用繼承的方式來達到這個目的,例如:
class FriedChicken {
double price() {
return 49.0;
}
String toString() {
return "炸雞";
}
}
class FriedChickenHamburger extends FriedChicken {
double price() {
return super.price() + 30.0;
}
String toString() {
return "%s | %s".formatted(super.toString(), "漢堡");
}
}
組合優於繼承
經常有人說不要濫用繼承,因為繼承具有較高的約束性,特別是在只能單一繼承的語言,這個設計為例,繼承父類別之後,只是取得父類別的 price
執行結果進一步處理,另一方面,如果漢堡也想要搭配附餐一,目前的 ComboA
顯然無法給漢堡重用,還得為漢堡建立有附餐一的子類別。
可以的話,在使用繼承之前先想想有沒有替代方案,例如改為組合的方式:
interface Meal {
double price();
}
class FriedChicken implements Meal {
public double price() {
return 49.0;
}
String toString() {
return "不黑心炸雞";
}
}
class Hamburger implements Meal {
public double price() {
return 99.0;
}
public String toString() {
return "美味蟹堡";
}
}
abstract class Combo implements Meal {
protected Meal meal;
Combo(Meal meal) {
this.meal = meal;
}
}
class ComboA extends Combo {
ComboA(Meal meal) {
super(meal);
}
public double price() {
return meal.price() + 30.0;
}
public String toString() {
return "A 套餐:%s | %s | %s".formatted(meal.toString(), "可樂", "薯條");
}
}
你可以如上設計 ComboB
、ComboC
等,後續怎麼搭都可以了:
meal1 = new ComboA(new FriedChicken());
var meal2 = new ComboA(new Hamburger());
var meal3 = new ComboB(new FriedChicken());
out.printf("%s: $%f", meal1.toString(), meal1.price());
out.printf("%s: $%f", meal2.toString(), meal2.price());
out.printf("%s: $%f", meal3.toString(), meal3.price());
這樣的設計是組合優於繼承(composite over inheritance)的實現,這邊的組合與 Gof 的 Composite 模式沒有關係,只是英文正好都使用 composite,在 Gof 中稱這個模式為 Decorator。
java.io 的 Decorator
在 java.io
套件中,有些輸入輸出的功能修飾,就是採用 Decorator 來實現,例如:
var reader = new BufferedReader(new FileReader("Main.java"));
FileReader
沒有緩衝區處理的功能,可以由 BufferedReader
提供,BufferedReader
沒有改變 FileReader
的功能,是在既有 FileReader
的操作成果上再做加工,而 BufferedReader
也不只可以用於 FileReader
,只要是 Reader
的子類別,都可以套用 BufferedReader
,例如讀取標準輸入:
var reader = new BufferedReader(new InputStreamReader(System.in));
InputStreamReader
修飾了 System.in
,BufferedReader
修飾了 InputStreamReader
,也就是說,功能可以視需求而堆疊。
Python 的標註
雖然 Gof 示範設計模式時,是基於物件導向典範,使用 C++ 實作範例,不過,那只是模式最後因應典範或語言的實現外觀,你不應該將焦點放在模式的實現外觀,應該多去思考模式構成過程中,是基於什麼樣的本質而成形。
Decorator 本質上就是可以指定一組演算,將該演算封裝,傳回另一組演算。
在物件導向中,物件就是封裝一組演算,建立一個物件封裝某個物件,就是封裝了一組演算,作為封裝者的物件,基於被封裝物件提供另一組演算。
物件導向的優點之一是,可以將一組演算與資料封裝在一起,然而,在一些不需要演算與資料封裝在一起的場合,函式更為適合,函式單純用來封裝演算,有的語言中,函式可以當成值傳遞,如果一個函式可以接受函式,傳回另一個函式,有人稱這種「接受函式傳回函式」的函式為高階函式(high order function)。
如果高階函式接受的函式,被封裝在傳回的函式中,也就有了 Decorator 的概念了。
來用一開始的點餐程式作為範例好了,假設你設計了點餐程式,目前主餐有炸雞,價格為 49 元:
def friedchicken():
return 49.0
print(friedchicken()) # 49.0
之後在程式中其他幾個地方都呼叫了 friedchicken
函式,若現在打算推套餐該怎麼做?修改 friedchicken
函式?另外增加一個 friedchicken_hamburger
函式?也許你的主餐不只有炸雞,還有漢堡、義大利麵等各式主餐呢!套餐也會有各式各樣?
Python 的函式是一級值,函式可以接受函式並傳回函式,可以這麼撰寫:
def combo_a(meal):
return lambda: meal() + 30
def friedchicken():
return 49.0
combo_a = combo_a(friedchicken)
print(combo_a()) # 顯示 79.0
這就實現了 Decorator 的概念,如果現在,炸雞不能單點了,直接推 A 套餐,Python 還可以這麼寫:
def combo_a(meal):
return lambda: meal() + 30
@combo_a
def friedchicken():
return 49.0
print(friedchicken()) # 顯示 79.0
@
可以接上函式,對於底下的程式碼,若 decorator
是個函式:
@decorator
def func():
...
執行時結果相當於:
func = decorator(func)
這也就是為什麼,Python 中這種 @decorator
的語法,會被稱為 decorator,必要時也可以堆疊:
@decorator2
@decorator1
def func():
...
實際上,Python 在實現這類功能時,不一定要使用函式,在更複雜的需求下,也可以實作〈callable 物件〉。
這也就是為什麼之前一直在說,不要過於著墨模式最後的實現形式,因為模式的實現會因語言而異,重點在於思考,才能在各種語言、需求下靈活變化,只要你有思考過,長得像或不像 Gof 設計模式或某文件說的 xx 模式都無所謂。
例如 Scala 可以這麼實現 Decorator 模式:
val meal1 = new FriedChicken with ComboA
val meal2 = new FriedChicken with ComboB
val meal3 = new FriedChicken with ComboA with ComboC
記得,你要觀察、思考,就這邊而言,就是觀察、思考,你需要的是不是指定一組演算,將該演算封裝,傳回另一組演算呢?