阅读《大话设计模式》和《精通 Swift 设计模式》中的装饰器模式,本文为笔记。

简介

装饰器模式,可以用于在运行时选择性地修改对象的行为,在处理无法修改的类时能发挥其强大的能力。

我们可以在想修改对象的行为时,又不想修改对象所属的类或其使用者,就可以使用装饰器模式。如果想修改对象的类实现,则不推荐使用装饰器模式,此时直接修改类往往更简单。

例子

阅读前请下载 OS X 命令工具行初始项目 Decorator_start,项目中注释较详细,如有问题请联系我。

项目地址:Decorator_in_Swift

初始项目 UML 图

Purchase 类表示顾客在商店买了什么,其中定义了两个属性来存储商品名称和价格,还有两个计算属性把信息提供给外界。

CustomerAccount 类表示一组 Purchase 对象,代表了顾客所购买的商品,addPurchase(Purchase)方法代表顾客购买了新商品。

Options 类中包含三个类,为前两个类提供了礼品服务,例如:2 元的礼品包装、1 元的彩带和 5 元的礼品配送。这里利用继承在 Options 类创建了三个装饰器类解决一个小问题——在不修改原来的两个类时添加了礼品服务功能。

Purchase 类和 CustomerAccount 类实现了创建一个用户对象,然后买一个特定价格的商品,如:张三购买了 10 元的帽子。后三个类则扩展了功能,能为每一个商品增加一个礼品服务的选项,如:张三购买了有彩带包装的 10 元的帽子。

但是已有的代码实现不了为一个商品添加多个礼品服务,如:张三购买了有礼品和彩带包装的 10 元的帽子。

源码运行结果:

1
2
3
4
Purchase Red Hat, Price ¥10.00
Purchase Scarf, Price ¥20.00
Purchase Scarf + delivery, Price ¥25.00
Total due: ¥55.00

装饰器模式

装饰器模式通过创建装饰器类解决上述组合问题(礼品包装+彩带、礼品包装+彩带+礼品配送等),装饰器类是指用于封装原始类并改变其行为的类。

装饰器类提供的 API 和封装的原始类相同,为了创建其他组合,装饰器还可以封装其他装饰器。

这里的单个礼品服务类 Options.swift 可以看做是一个小小的装饰器。

装饰器模式

实现

装饰器类需要继承无法修改的类,来创建一个拥有该类所有方法和属性的类,用来替换原始类的功能。装饰器需要定义一个用来存储被封装对象的私有属性,从而为外界提供该对象的基本功能。

Options.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/// 装饰器
class BasePurchasDecorator : Purchase {
private let wrappedPurchase: Purchase

init(purchase: Purchase) {
wrappedPurchase = purchase
super.init(product: purchase.description, price: purchase.totalPrice)
}
}

/// 礼品包装
class PurchaseWithGiftWrap : BasePurchasDecorator {
override var description: String {
return "\(super.description) + giftwrap"
}

override var totalPrice: Float {
return super.totalPrice + 2
}
}

/// 礼带
class PurchaseWithRibbon : BasePurchasDecorator {
override var description: String {
return "\(super.description) + ribbon"
}

override var totalPrice: Float {
return super.totalPrice + 1
}
}

/// 礼品配送
class PurchaseWithDelivery : BasePurchasDecorator {
override var description: String {
return "\(super.description) + delivery"
}

override var totalPrice: Float {
return super.totalPrice + 5
}
}

这里在开头创建了一个继承自 Purchase 类的礼品服务的基类 BasePurchasDecorator,并把原有礼品服务类的父类修改成了它。在 BasePurchasDecorator 类中,不仅定义了一个私有属性 wrappedPurchase 来保存 Purchase 对象(增加新功能小节能看到用途),还会对礼品服务类的属性(descriptiontotalPrice)进行处理,以供继承的子类去根据自身的配送描述和价格修改其内容。

main.swift 修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let account = CustomerAccount(name: "Joe")
account.addPurchase(Purchase(product: "Red Hat", price: 10))
account.addPurchase(Purchase(product: "Scarf", price: 20))
// 礼品配送服务
//account.addPurchase(PurchaseWithDelivery(product: "Scarf", price: 20))
// 带礼品配送服务的20元围巾
account.addPurchase(
PurchaseWithDelivery(purchase:
Purchase(product: "Scarf", price: 20)))

// 带礼品包装和礼品配送服务的25元太阳眼镜
account.addPurchase(
PurchaseWithDelivery(purchase:
PurchaseWithGiftWrap(purchase:
Purchase(product: "Sunglasses", price:25))))

account.printAccount()
1
2
3
4
5
Purchase Red Hat, Price ¥10.00
Purchase Scarf, Price ¥20.00
Purchase Scarf + delivery, Price ¥25.00
Purchase Sunglasses + giftwrap + delivery, Price ¥32.00
Total due: ¥87.00

增加新功能

如果我们还想要用装饰器为原对象增加新的方法或属性,例如节日打折,我们可以继续定义一个折扣装饰器 DiscountDecorator,并创建子类实现不同折扣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/// 新增了折扣功能的装饰器
class DiscountDecorator: Purchase {
private let wrappedPurchase: Purchase

init(purchase: Purchase) {
self.wrappedPurchase = purchase
super.init(product: purchase.description, price: purchase.totalPrice)
}

override var description: String {
return super.description
}

/// 优惠了多少元
var discountAmount: Float {
return 0
}

/// 判断产品是否适用折扣优惠,计算已用折扣数
func countDiscounts() -> Int {
var total = 1
// 注意这里对象本身就已经是一个折扣,wrappedPurchase 是折扣后跟着的purchase对象,还是折扣的话折扣数+1
if let discounter = wrappedPurchase as? DiscountDecorator {
total += discounter.countDiscounts()
}
return total
}
}

/// 黑色星期五打8折
class BlackFridayDecorator: DiscountDecorator {
override var totalPrice: Float {
return super.totalPrice - discountAmount
}

override var discountAmount: Float {
return super.totalPrice * 0.20
}
}

/// 清仓大甩卖打3折
class EndOfLineDecorator: DiscountDecorator {
override var totalPrice: Float {
return super.totalPrice - discountAmount
}

override var discountAmount: Float {
return super.totalPrice * 0.70
}
}

其中我们根据 wrappedPurchase 属性是否继承自折扣装饰器,来判断方法所包含的对象是否是有折扣的商品。例如下面代码中的 EndOfLineDecorator(Purchase) 方法所包含的对象: BlackFirdayDecorator(purchase: PurchaseWithDelivery(purchase: PurchaseWithGiftWrap(purchase: Purchase(product: "Towel", price: 12)))) 有黑五折扣的礼品配送和礼品包装服务的12元太阳眼镜。

main.swift 中新增用了折扣的商品的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//... 忽略部分代码

// 带礼品配送+礼品包装服务的12元太阳眼镜共19元,3折加8折后4.56元。
account.addPurchase(
EndOfLineDecorator(purchase:
BlackFridayDecorator(purchase:
PurchaseWithDelivery(purchase:
PurchaseWithGiftWrap(purchase:
Purchase(product: "Towel", price: 12))))))

account.printAccount()

// 输出每个商品的折扣数
for purchase in account.purchases {
if let discountPurchase = purchase as? DiscountDecorator {
print("\(discountPurchase) has \(discountPurchase.countDiscounts()) discounts")
} else {
print("\(purchase) has no discounts")
}
}
1
2
3
4
5
6
7
8
9
10
11
Purchase Red Hat, Price ¥10.00
Purchase Scarf, Price ¥20.00
Purchase Scarf + delivery, Price ¥25.00
Purchase Sunglasses + giftwrap + delivery, Price ¥32.00
Purchase Towel + giftwrap + delivery, Price ¥4.56
Total due: ¥91.56
Red Hat has no discounts
Scarf has no discounts
Scarf + delivery has no discounts
Sunglasses + giftwrap + delivery has no discounts
Towel + giftwrap + delivery has 2 discounts

UML 图

如果想要让折扣只作用在产品价格而不对礼品服务做影响,也是很简单的。

main.swift

1
2
3
4
5
6
7
// 带礼品配送+礼品包装服务的12元太阳眼镜共19元,黑五8折只对产品价格有效,之后再打清仓3折后4.98元。
account.addPurchase(
EndOfLineDecorator(purchase:
PurchaseWithDelivery(purchase:
PurchaseWithGiftWrap(purchase:
BlackFridayDecorator(purchase:
Purchase(product: "Towel", price: 12))))))
1
2
3
4
...
Purchase Towel + giftwrap + delivery, Price ¥4.98
...
Towel + giftwrap + delivery has 1 discounts

计算折扣数的时候有点小问题,但是价格是对的。

用装饰器的时候需要谨慎,要评估对应用其他部分带来哪些影响,特别是已经使用了其他装饰器的情况下。

合并装饰器

装饰器可以为原始类新增新功能,也能合并装饰器。

注意:

  • 装饰器的作用应该是增强或者拓展原始类的功能,而不是给现有的 API 渗透功能。
  • 小项目中直接用多个独立的装饰器类会比较简单,对于复杂的项目会不便于维护,所以将相关联的装饰器类合并会比较合适。

Options.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// 合并装饰器
class GiftOptionDecorator: Purchase {
private let wrappedPurchase: Purchase
private let options: [Option]

enum Option {
case giftWrap
case ribbon
case delivery
}

init(purchase: Purchase, options: [Option]) {
self.wrappedPurchase = purchase
self.options = options
super.init(product: purchase.description, price: purchase.totalPrice)
}

override var description: String {
var result = wrappedPurchase.description
for option in options {
switch option {
case .giftWrap:
result = "\(result) + giftwrap"
case .ribbon:
result = "\(result) + ribbon"
case .delivery:
result = "\(result) + delivery"
}
}
return result
}

override var totalPrice: Float {
var result = wrappedPurchase.totalPrice
for option in options {
switch option {
case .giftWrap:
result += 2
case .ribbon:
result += 1
case .delivery:
result += 5
}
}
return result
}
}

main.swift

1
2
3
4
5
6
7
// 利用合并装饰器实现:带礼品配送+礼品包装服务的12元太阳眼镜共19元,3折加8折后4.56元。
account.addPurchase(EndOfLineDecorator(purchase:
BlackFridayDecorator(purchase:
GiftOptionDecorator(purchase:
Purchase(product: "Towel", price: 12),
options: [GiftOptionDecorator.Option.giftWrap,
GiftOptionDecorator.Option.delivery]))))
1
2
3
4
...
Purchase Towel + giftwrap + delivery, Price ¥4.56
...
Towel + giftwrap + delivery has 2 discounts

完整项目 UML 图

结果一样,但是实现方式简洁了不少。

合并装饰器的优点是能把类的核心职责和装饰功能区分开,去除相关类重复的装饰逻辑。

总结

完整的项目放在 Decorator_end

如果我们错误地实现了装饰器模式,那么装饰器所做的修改会对所有对象产生影响,或者另应用多出一些和对象无关的功能。

增加新功能小节中所提及的,实现装饰器模式后,要注意装饰顺序,否则不同的折扣就应用到不同的范围上了。

重申一遍,装饰器模式是在已有功能上加更多功能的一种方式,不能修改原始类的已有功能和属性。