装饰者模式¶
分类:结构型模式。来源:《Head First 设计模式》第 3 章。
类结构¶
classDiagram
direction TB
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
<<abstract>>
+operation()
}
class ConcreteDecorator {
+operation()
+addedBehavior()
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Decorator <|-- ConcreteDecorator
Decorator --> Component : 包装
角色定义¶
| 角色 | 定义 |
|---|---|
| Component(组件接口) | 定义被装饰对象与装饰者共同遵守的操作契约。 |
| ConcreteComponent(具体组件) | 实现组件接口,提供需要动态扩展的基础行为。 |
| Decorator(抽象装饰者) | 实现组件接口并持有另一个组件,将操作委托给被包装对象;该对象也可以是另一个装饰者。 |
| ConcreteDecorator(具体装饰者) | 继承抽象装饰者,在委托操作之前或之后添加职责,同时保持组件接口。 |
原书笔记¶
开放-关闭原则¶
设计原则:类应该对扩展开放,对修改关闭。
我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如果实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
认识装饰者模式¶
以饮料为主体,然后在运行时以调料来“装饰”饮料。比方说,如果顾客想要摩卡和奶泡深焙咖啡,那么,要做的是:
- 拿一个深焙咖啡(DarkRoast)对象
- 以摩卡(Mocha)对象装饰它
- 以奶泡(Whip)对象装饰它
- 调用
cost()方法,并依赖委托(delegate)将调料的价钱加上去。
目前所知的一切:
- 装饰者和被装饰对象有相同的超类型,我们利用继承达到“类型匹配”,而不是利用继承获得“行为”,因为装饰者必须能取代被装饰者。
- 你可以用一个或多个装饰者包装一个对象
- 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象的场合,可以用装饰过的对象代替它。
- 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
- 对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用你喜欢的装饰者来装饰对象。
定义装饰者模式¶
装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
写下星巴兹的代码¶
实现Condiment(调料)抽象类,也就是装饰者类吧:
饮料的代码
调料代码
供应咖啡¶
输出结果
真实世界的装饰者:Java I/O¶
装饰java.io类¶
编写自己的Java I/O装饰者¶
测试你的新Java I/O装饰者¶
输出结果
Head First 示例:星巴兹的双份摩卡奶泡咖啡¶
《Head First 设计模式》第 3 章用星巴兹点单说明装饰者:同一杯深焙咖啡可以逐层加入两份摩卡和一份奶泡,每层都保持饮料类型,并在基础价格上追加自己的费用。这里选取作者 2020 年第二版配套代码 decorator/starbuzz 中 StarbuzzCoffee 的第二杯饮料,不展开其他饮料和调料组合。
本节按该版本源码核对:Beverage 是抽象类;CondimentDecorator 继承它并声明被包装对象字段 beverage;Milk、Soy、Whip 都继承 CondimentDecorator。这些细节与上方保留的历史代码片段有所不同,下面的示例图以本节链接的固定版本为准。
| 具体类名 | 对应角色 | 职责 |
|---|---|---|
Beverage |
抽象组件 | 提供 getDescription(),声明抽象方法 cost(),统一饮料与调料的使用方式。 |
DarkRoast |
具体组件 | 设置深焙咖啡描述,返回基础价格 0.99。 |
CondimentDecorator |
抽象装饰者 | 持有 Beverage beverage,要求子类实现 getDescription()。 |
Mocha |
具体装饰者 | 委托给被包装饮料,再追加摩卡描述和 0.20 的费用。 |
Whip |
具体装饰者 | 委托给被包装饮料,再追加奶泡描述和 0.10 的费用。 |
StarbuzzCoffee |
客户端与示例入口 | 逐层包装饮料,通过 Beverage 读取最终描述与价格。 |
示例 UML¶
classDiagram
direction TB
class Beverage {
<<abstract>>
String description
+getDescription() String
+cost() double*
}
class DarkRoast {
+cost() double
}
class CondimentDecorator {
<<abstract>>
Beverage beverage
+getDescription() String*
}
class Mocha {
+Mocha(Beverage beverage)
+getDescription() String
+cost() double
}
class Whip {
+Whip(Beverage beverage)
+getDescription() String
+cost() double
}
class StarbuzzCoffee {
+main(String[] args)$
}
Beverage <|-- DarkRoast
Beverage <|-- CondimentDecorator
CondimentDecorator <|-- Mocha
CondimentDecorator <|-- Whip
CondimentDecorator --> Beverage : beverage 包装
StarbuzzCoffee ..> Beverage : 读取描述与价格
StarbuzzCoffee ..> DarkRoast : 创建基础饮料
StarbuzzCoffee ..> Mocha : 包装两次
StarbuzzCoffee ..> Whip : 包装最外层
图中只保留双份摩卡奶泡深焙咖啡所需类型,省略其他饮料、调料和带杯型的另一个示例包。继承使调料拥有饮料类型,beverage 关联使其能够在运行时包住另一个饮料或装饰者。
协作过程¶
StarbuzzCoffee创建DarkRoast,用Beverage变量保存它。- 连续两次创建
Mocha,每次把当前饮料传入构造器,再将变量指向新的包装层;最后用Whip包装最外层。 - 调用最外层
getDescription(),各层先取得内层描述再追加自己的调料名称,因此两份摩卡都会出现在结果中。 - 调用最外层
cost(),费用沿包装链累计为0.99 + 0.20 + 0.20 + 0.10 = 1.49;入口打印最终描述和价格。
固定版本源码: