什么是装饰者模式?
装饰者模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰者模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
认识装饰者模式
从一个例子准备了解装饰者模式
虽然笔者并不喜欢喝奶茶,但奶茶店在街道上随处可见。假如你要开一家奶茶店,会怎么设计奶茶相关的类呢?
最初的设计
下面为奶茶中涉及到的类的设计:
购买奶茶时,光有奶茶还不够,还可以要求在里面加入各种好吃的配料,如黑珍珠(BlackPearl),冰块(IceCake),冰淇凌(IceCream),芒果块(MongoBlock)等等。
由于奶茶店会根据所加入的配料收取不同的费用,所以订单系统必须考虑到这些配料部分,因此原先设计的要适当变化:
很明显,这样设计会为我们后期维护带来相当大的困难,如果黑珍珠的价格上涨,怎么办?新增一种新的布丁配料时又怎么办?新手才会这么设计。
我们不应该设计这么多的类,若使用实例变量和继承,是否就可以追踪这些配料呢?
改进后的奶茶类
因此,我们重新改造一下,将MilkyTea基类加上实例变量,代表是否加上配料(黑珍珠、冰块、冰淇凌、芒果块等等):
MilkyTea基类实现的代码: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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68public class MilkyTea {
protected String description;
private double blackPearlCost = 1.0;
private double iceCakeCost = 1.0;
private double iceCreamCost = 2.0;
private double MongoBlackCost = 3.0;
private boolean blackPearl;
private boolean iceCake;
private boolean iceCream;
private boolean MongoBlack;
public double cost() {
double condimentCost = 0.0;
if (isBlackPearl()) {
condimentCost += blackPearlCost;
}
if (isIceCake()) {
condimentCost += iceCakeCost;
}
if (isIceCream()) {
condimentCost += iceCreamCost;
}
if (isMongoBlack()) {
condimentCost += MongoBlackCost;
}
return condimentCost;
}
public String getDescription() {
return description;
}
public boolean isBlackPearl() {
return blackPearl;
}
public void setBlackPearl(boolean blackPearl) {
this.blackPearl = blackPearl;
}
public boolean isIceCake() {
return iceCake;
}
public void setIceCake(boolean iceCake) {
this.iceCake = iceCake;
}
public boolean isIceCream() {
return iceCream;
}
public void setIceCream(boolean iceCream) {
this.iceCream = iceCream;
}
public boolean isMongoBlack() {
return MongoBlack;
}
public void setMongoBlack(boolean mongoBlack) {
MongoBlack = mongoBlack;
}
}
具体实现的 2 种奶茶类: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// 纯苹果奶茶什么也没加
public class AppleMilkyTea extends MilkyTea {
public AppleMilkyTea() {
description = "好喝的苹果奶茶";
}
// 就花了 2 块钱
public double cost() {
return 2.0 + super.cost();
}
}
// 苹果奶茶加了黑珍珠和冰块
public class AppleMilkyTeaWithBlackPearlAndIceCake extends MilkyTea {
public AppleMilkyTeaWithBlackPearlAndIceCake() {
setBlackPearl(true);
setIceCake(true);
description = "好喝的苹果奶茶还加了黑珍珠和冰块";
}
// 花了 2 块钱加上配料的钱
public double cost() {
return 2.0 + super.cost();
}
}
虽然改进后的类比原来更具有弹性了,但是当有些需求或因素改变时还是会影响这个设计。
比如配料价钱的改变会使我们更改现有代码。一旦出现新的配料,我们就需要加上新的方法,并改变父类中的cost()方法。而且以后可能会有新类型的奶茶,对某些奶茶而言,和有些配料是不能混在一起吃的(比如菠萝奶茶和芒果配料是不能一起吃的)。
但是在这个设计方式中,奶茶的子类仍将继承那些不合适的方法(虽然这些方法可能不会使用)。因此,我们需要使用装饰者模式。
着手解决问题
从前面我们知道利用继承无法完全解决问题:类数量爆炸、设计死板,以及父类加入的新功能并不适用于所有的子类。
所以,在这里我们要采用不一样的做法:以奶茶为主体,然后在运行时以配料来“装饰”奶茶。
比如,顾客想要蓝莓奶茶加黑珍珠和芒果块,那么需要:
① 拿一个蓝莓奶茶对象
② 以黑珍珠对象装饰它
③ 以芒果块对象装饰它
④ 调用cost()方法,并依赖委托将配料的价钱加上去。
那么如何“装饰”一个对象?“委托”又如何与此搭配使用?
很简单,我们可以把装饰者对象当成“包装者”。
以装饰者构造奶茶订单
① 以BlueberryMilkyTea对象为开始(蓝莓奶茶继承自奶茶,且有一个计算价钱的cost()方法):
② 顾客还想要黑珍珠(BlackPearl),所以建立一个BlackPearl对象,并用它将BlueberryMilkyTea对象包装起来(BlackPearl对象是一个装饰者,它的类型“反映”了它所装饰的MilkyTea对象。“反映”即指两者类型一致。通过多态可以把BlackPearl所包裹的任何类型的MilkyTea当成是MilkyTea)。
③ 顾客又想要芒果块(MongoBlack),所以需要建立一个MongoBlack对象,并用它把BlackPearl对象包装起来
④ 现在为顾客计算花费的价钱。通过调用最外圈装饰者(MongoBlack)的cost()就可以办得到。MongoBlack的cost()会先委托它装饰的对象也就是BlackPearl计算出价钱,再加上芒果块的价钱,具体如下图:
从上面我们可以知道:
- 装饰者和被装饰对象有相同的父类
- 可以用一个或多个装饰者包装一个对象
- 既然装饰者和被装饰对象有相同的父类,那么可以在任何需要原始对象的场合,用装饰过的对象代替它。
- 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定目的
- 对象可以在任何时候被装饰,所以可以在运行时动态的、不限量地用你喜欢的装饰者来装饰对象
定义装饰者模式
装饰者模式动态地将责任附加到对象上。 若要扩展功能,装饰者提供了比继承更有弹性 的替代方案。
下面为装饰者的类图,我们后面会套用此结构:
装饰者模式改进的例子
现在让我们的奶茶符合上面定义的结构,类图如下:
从类图我们看到,CondimentDecorator扩展自MilkyTea类,用到了继承。这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的父类,这是相当关键的地方。这里的继承是达到“类型匹配”,而不是利用继承获得“行为”。
那么行为又是从哪里来的呢?
当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自父类,而是由组合对象得来的。也就是说,继承MilkyTea类,是为了有正确的类型,而不是继承它的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。
因为使用对象组合,就可以把所有奶茶和配料更有弹性地加以混合和匹配,非常方便。如果依赖继承,那么类的行为只能在编译时静态决定,行为不是来自父类,就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合使用,而且是在“运行时”!!!
为什么不把MilkyTea设计成一个接口,而是抽象类呢?
通常装饰者模式是采用抽象类,但是在 Java 中也可以使用接口。
代码具体实现
现在开始正在设计代码了,首先从MilkyTea切入:1
2
3
4
5
6
7
8
9public abstract class MilkyTea {
String description = "白开水状态的奶茶。。。";
public String getDescription(){
return description;
}
public abstract double cost();
}
然后实现CondimentDecorator抽象类,也就是装饰者类,黑珍珠冰块等配料都继承该类:1
2
3
4public abstract class CondimentDecorator extends MilkyTea {
// 所以的配料都必须重新实现该方法
public abstract String getDescription();
}
接着开始写一些奶茶类吧,需要将具体的奶茶描述一下哦(类别描述和价钱花费),好让顾客选择,代码如下: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// 苹果奶茶
public class AppleMilkyTea extends MilkyTea {
// 描述奶茶具体的种类
public AppleMilkyTea() {
description = "这是苹果奶茶";
}
// 奶茶的价钱花费
public double cost() {
return 1.0;
}
}
// 蓝莓奶茶
public class BlueberryMilkyTea extends MilkyTea {
public BlueberryMilkyTea() {
description = "这是蓝莓奶茶";
}
public double cost() {
return 3.0;
}
}
完成了抽象组件(MIlkyTea),有了具体组件(AppleMilkyTea、BlueberryMilkyTea),也有了装饰者(CondimentDecorator),紧接着就是实现具体装饰者配料类了,顾客说:往我的奶茶里加点黑珍珠和冰块。好嘞: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// 黑珍珠
public class BlackPearl extends CondimentDecorator {
// 用一个实例变量记录奶茶,也就是被装饰者
MilkyTea milkyTea;
// 想办法让被装饰者被记录到实例变量中,把奶茶当作构造器参数传入即可,再记录给实例变量
public BlackPearl(MilkyTea milkyTea) {
this.milkyTea = milkyTea;
}
/**
* 不仅仅描述了奶茶,我们还得完整地描述配料
* 因此首先利用委托的方法,在 CondimentDecorator 得到一个描述
* 再在其子类附加具体的描述
* @return
*/
public String getDescription() {
return milkyTea.getDescription() + ",并且加了黑珍珠";
}
// 自然也要计算配料的价钱
public double cost() {
return 1.0 + milkyTea.cost();
}
}
// 冰块
public class IceCake extends CondimentDecorator {
MilkyTea milkyTea;
public IceCake(MilkyTea milkyTea) {
this.milkyTea = milkyTea;
}
public String getDescription() {
return milkyTea.getDescription() + ",并且加了冰块";
}
public double cost() {
return 1.0 + milkyTea.cost();
}
}
最后就是测试类了:1
2
3
4
5
6
7
8
9
10
11 // 一个什么都没加的苹果奶茶
MilkyTea milkyTea = new AppleMilkyTea();
System.out.println(milkyTea.getDescription() + ",价钱人民币 " + milkyTea.cost() + " 块");
// 创建一杯蓝莓奶茶
MilkyTea milkyTea1 = new BlueberryMilkyTea();
// 用黑珍珠装饰它
milkyTea1 = new BlackPearl(milkyTea1);
// 再用冰块装饰它
milkyTea1 = new IceCake(milkyTea1);
System.out.println(milkyTea1.getDescription() + ",价钱人民币 " + milkyTea1.cost() + " 块");
测试类运行结果如下:1
2这是苹果奶茶,价钱人民币 1.0 块
这是蓝莓奶茶,并且加了黑珍珠,并且加了冰块,价钱人民币 5.0 块
装饰者模式的缺点
硬币有正反两面,装饰者模式也有一个“缺点”:利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用相关 API (如java.io)程序员的困扰。
Java 中的装饰者:I/O 类
java.io类非常多,其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:
BufferedInputStream及LineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类。
让我们查看一下各种 I/O 类之间的关系:
可以发现,其和奶茶店的设计相比其实并没有多大的差异。将java.io API 范围缩小,可以容易的查看它的文件,并组合各种“输入”流装饰者来符合我们的用途。
类似地,“输出”流的设计方式也是一样的,而且字符流的设计和字节流的设计也相当类似(有一点小差异),知道装饰者模式之后,可以更好地理解这些类。
参考
- Eric Freeman. HeadFirst 设计模式 [M]. 中国电力出版社,2007
文章信息
| 时间 | 说明 |
|---|---|
| 2019-04-22 | 初稿 |
