如果在swift中按部就班的談Gof設計模式,這在一開始就是錯誤的命題。原因主要有兩個:
- 設計模式是基於物件導向的程式設計正規化
- 實現基於當時的主流程式語言:C++ 和 Java
如今的swift的推薦程式設計正規化並不是物件導向,很多人都大談面向協議、函數語言程式設計我就不展開了;現代的swift中有一些語法特性是當時的語言所不具備的,比如protocol extension,高階函式等。
所以本文將利用swift的語法來談下裝飾器模式在swift下的解決思路。
Decorator pattern
有人翻作裝飾器模式,也有翻成裝飾者模式,英文的名稱就是Decorator pattern。
裝飾器模式能夠實現動態的為物件新增功能,是從一個物件外部來給物件新增功能。裝飾器模式就是基於物件組合的方式,可以很靈活的給物件新增所需要的功能。
直接用程式碼來說明。
用Dish表示菜餚,一道菜有兩個屬性,名稱和價格。然後為了方便測試重寫了description屬性,返回名稱和價格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Dish: CustomStringConvertible { var name: String var price: Int init(name: String, price: Int) { self.name = name self.price = price } var description: String { get { return "\(name): \(price)元" } } } |
再假設有一個菜的訂單物件,接收一個Dish物件後,最後可以通過total方法返回這些菜的總價錢。具體就不實現了,大概這個邏輯。
1 2 3 4 5 6 7 8 9 10 |
class DishOrder { func append(dish: Dish) { } func total() -> Int { } } |
假設在一個飯館裡,老闆發現這裡的客人點菜的時候喜歡讓廚師多放點鹽,大家都知道非典時期鹽很貴,所以老闆覺得很虧,決定如果一道菜多加鹽就要貴一塊錢。接著又來了一個需求,如果打包帶走,一道菜再加兩塊錢。如果我們不能改變Dish的原始碼(在實際專案中常會遇到這種情況,可能這類定義在第三方的庫裡),要怎麼實現這兩個需求呢?
可以定義兩個裝飾器,注意這兩個裝飾器都要繼承Dish:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 加鹽的裝飾器 class SaltDishDecorator: Dish { init(dish: Dish) { super.init(name:"加糖 \(dish.name)", price: dish.price + 1) } } // 外帶打包的裝飾器 class PackageDishDecorator: Dish { init(dish: Dish) { super.init(name:"打包 \(dish.name)", price: dish.price + 2) } } |
現在我們要表示一道打包帶走的松鼠桂魚就這樣表示了:
1 |
let dish = PackageDishDecorator(dish: SaltDishDecorator(dish: Dish(name: "松鼠桂魚", price: 15))) |
這樣我們就可以給任意一道菜增加一些裝飾性的功能。也有人用咖啡舉例子,一杯咖啡可能要加糖,加奶,加巧克力等等。一層包一層。如果取名字的是中國人可能就叫洋蔥模式了。最後使用的時候行為和Dish是一樣的。因為這些Decorator是繼承自Dish的。只是在初始化過程中改變了原有的一些屬性。
缺陷:繼承不是一個優秀的解決方案
程式設計時常常提到的一個指導思想就是組合優於繼承。
繼承最大的問題就在於你只能有一個爹。一個爹的結果就是能力有限,不夠靈活。所以最後還得認一些乾爹。
就拿上面的例子來講,現在是給Dish做了幾個裝飾功能,如果有一天說店裡的點心也要支援這兩個功能(給我來一個加鹽的饅頭!),是不是有要繼承點心類寫兩個裝飾器呢?
利用Swift中的Extension
我們可以這麼理解這個需求,需要有這麼一個方法,接受一個Dish型別的引數,經過處理後返回一個Dish。我們完全可以把這個方法通過extension寫在Dish身上。
1 2 3 4 5 6 7 8 9 10 |
extension Dish { func salted() -> Dish { return Dish(name:"加鹽 \(name)", price: price + 1) } func packaged() -> Dish { return Dish(name:"打包 \(name)", price: price + 2) } } |
然後我們就可以鏈式呼叫:
1 |
let extenedDish = Dish(name: "松鼠桂魚", price: 15).salted().packaged() |
更進一步:protocol extension
如果為了將來的擴充套件靈活,也可以把這個裝飾寫到protocol的extension裡。
1 2 3 4 5 6 7 8 |
protocol Product { var name: String { get set } var price: Int { get set } } protocol Salteable: Product { func salted() -> Self } |
上面先定義了一個產品的協議,有名稱和價格兩個屬性。
接著再定義了一個繼承Product的Salteable的協議。裡面有一個返回自身的salted方法。接著給這個protocol增加擴充套件實現:
1 2 3 4 5 6 7 8 |
extension Salteable { func salted() -> Self { var newProduct = self newProduct.name = "加鹽 \(name)" newProduct.price = price + 1 return newProduct } } |
然後我們再定義一個表示小吃的Snack,和菜一樣也有兩個屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct Snack: CustomStringConvertible { var name: String var price: Int init(name: String, price: Int) { self.name = name self.price = price } var description: String { get { return "\(name): \(price)元" } } } |
如果我們要給這個Snack增加加鹽的效果,只要宣告他實現Salteable協議就可以了。
1 2 3 |
extension Snack: Salteable { } |
這樣就夠啦!
看下輸出:
歡迎關注我的微博:@沒故事的卓同學