设计模式之装饰者(Decorate)模式

思维导图

什么是装饰者模式?

  装饰者模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰者模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

认识装饰者模式

从一个例子准备了解装饰者模式

  虽然笔者并不喜欢喝奶茶,但奶茶店在街道上随处可见。假如你要开一家奶茶店,会怎么设计奶茶相关的类呢?

最初的设计

  下面为奶茶中涉及到的类的设计:

  购买奶茶时,光有奶茶还不够,还可以要求在里面加入各种好吃的配料,如黑珍珠(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
68
public 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 块钱
@Override
public double cost() {
return 2.0 + super.cost();
}
}

// 苹果奶茶加了黑珍珠和冰块
public class AppleMilkyTeaWithBlackPearlAndIceCake extends MilkyTea {
public AppleMilkyTeaWithBlackPearlAndIceCake() {
setBlackPearl(true);
setIceCake(true);
description = "好喝的苹果奶茶还加了黑珍珠和冰块";
}
// 花了 2 块钱加上配料的钱
@Override
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()就可以办得到。MongoBlackcost()会先委托它装饰的对象也就是BlackPearl计算出价钱,再加上芒果块的价钱,具体如下图:

  从上面我们可以知道:

  • 装饰者和被装饰对象有相同的父类
  • 可以用一个或多个装饰者包装一个对象
  • 既然装饰者和被装饰对象有相同的父类,那么可以在任何需要原始对象的场合,用装饰过的对象代替它。
  • 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定目的
  • 对象可以在任何时候被装饰,所以可以在运行时动态的、不限量地用你喜欢的装饰者来装饰对象

定义装饰者模式

   装饰者模式动态地将责任附加到对象上。 若要扩展功能,装饰者提供了比继承更有弹性 的替代方案。

  下面为装饰者的类图,我们后面会套用此结构:
简化的类图

装饰者模式改进的例子

  现在让我们的奶茶符合上面定义的结构,类图如下:

  从类图我们看到,CondimentDecorator扩展自MilkyTea类,用到了继承。这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的父类,这是相当关键的地方。这里的继承是达到“类型匹配”,而不是利用继承获得“行为”。
  那么行为又是从哪里来的呢?
  当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自父类,而是由组合对象得来的。也就是说,继承MilkyTea类,是为了有正确的类型,而不是继承它的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。
  因为使用对象组合,就可以把所有奶茶和配料更有弹性地加以混合和匹配,非常方便。如果依赖继承,那么类的行为只能在编译时静态决定,行为不是来自父类,就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合使用,而且是在“运行时”!!!
  为什么不把MilkyTea设计成一个接口,而是抽象类呢?
  通常装饰者模式是采用抽象类,但是在 Java 中也可以使用接口。

代码具体实现

  现在开始正在设计代码了,首先从MilkyTea切入:

1
2
3
4
5
6
7
8
9
public abstract class MilkyTea {
String description = "白开水状态的奶茶。。。";

public String getDescription(){
return description;
}

public abstract double cost();
}

  然后实现CondimentDecorator抽象类,也就是装饰者类,黑珍珠冰块等配料都继承该类:

1
2
3
4
public 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 = "这是苹果奶茶";
}

// 奶茶的价钱花费
@Override
public double cost() {
return 1.0;
}
}

// 蓝莓奶茶
public class BlueberryMilkyTea extends MilkyTea {

public BlueberryMilkyTea() {
description = "这是蓝莓奶茶";
}

@Override
public double cost() {
return 3.0;
}
}

  完成了抽象组件(MIlkyTea),有了具体组件(AppleMilkyTeaBlueberryMilkyTea),也有了装饰者(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
*/
@Override
public String getDescription() {
return milkyTea.getDescription() + ",并且加了黑珍珠";
}

// 自然也要计算配料的价钱
@Override
public double cost() {
return 1.0 + milkyTea.cost();
}
}

// 冰块
public class IceCake extends CondimentDecorator {

MilkyTea milkyTea;

public IceCake(MilkyTea milkyTea) {
this.milkyTea = milkyTea;
}

@Override
public String getDescription() {
return milkyTea.getDescription() + ",并且加了冰块";
}

@Override
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类非常多,其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:

  BufferedInputStreamLineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类。
  让我们查看一下各种 I/O 类之间的关系:

  可以发现,其和奶茶店的设计相比其实并没有多大的差异。将java.io API 范围缩小,可以容易的查看它的文件,并组合各种“输入”流装饰者来符合我们的用途。
  类似地,“输出”流的设计方式也是一样的,而且字符流的设计和字节流的设计也相当类似(有一点小差异),知道装饰者模式之后,可以更好地理解这些类。

参考

  • Eric Freeman. HeadFirst 设计模式 [M]. 中国电力出版社,2007

文章信息

时间 说明
2019-04-22 初稿
0%