flyEn'blog

设计模式(三)装饰者模式

本文为看《Head First设计模式》书后整理的技术分享第三篇。

装饰者模式

这个设计模式可以学到如何使用对象组合的方式,做到在运行时装饰类。

一旦你熟悉了装饰的技巧,就能在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。

设计原则

  • 类应该对扩展开放,对修改关闭。(开放-关闭原则)

举例

星巴兹是以扩张速度最快而闻名的咖啡连锁店。因为扩张速度实在太快了,他们准备更新订单系统,以满足他们的饮料供应要求。

img

购买咖啡时,也可以要求在其中加入各种调料,例如:牛奶、豆浆、摩卡或奶盖。星巴兹会根据所加入的调料收取不同的费用。

所以订单系统必须要考虑到这些调料部分。

这是他们的第一个尝试:

img

很明显,这简直是为自己制造了一个维护噩梦,如果牛奶涨价,怎么办?新增一种焦糖调味,怎么办?

所以他们做了第二个尝试。

image-20200903184817668

image-20200903184831022

当哪些需求或因素改变时会影响这个设计?

  • 调料价格的改变会使我们更改现有代码。
  • 一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法。
  • 以后可能会开发出新饮料,对这些饮料而言(比如冰茶),某些调料可能并不适合,但是在这个设计中,Tea子类仍将继承那些不合适的方法,例如:hasWhip(加奶泡)。
  • 万一顾客想要双倍摩卡的咖啡,怎么办?

我们已经了解到利用组合和委托可以在运行时具有继承行为的效果。

利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为,然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态进行扩展。

所以我们可以利用此技巧把多个新职责,甚至是设计超类时还没想到的职责加在对象上,而且,可以不用修改原来的代码。

🌟第五个设计原则:开放—关闭原则:类应该对扩展开放,对修改关闭。

认识装饰者模式

我们已经了解到利用继承无法完全解决问题,在星巴兹遇到的问题有:类数量爆炸、设计死板、以及基类加入的新功能并不适用于所有的子类。

所以在这里我们要以饮料为主体,然后在运行时以调料来“装饰”饮料。那么。要做的是:
1:拿一个深焙咖啡(DarkRoast)对象。
2:以摩卡(Mocha)对象装饰它。
3:以奶泡(Whip)对象装饰它。
4:调用cost()方法,并依赖委托(delegate)将调料的价钱加上去。

但是如何“装饰”一个对象,而“委托”又要如何与此搭配使用?(提示:把装饰者对象当成“包装者”)

以装饰者构造饮料订单

image-20200903203827784

image-20200903204141537

  1. 以DarkRoast对象开始。DarkRoast继承自Beverage,且有一个用来计算饮料价钱的cost()方法。
  2. 顾客想要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。
  3. 顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包装起来。别忘了,DarkRoast继承自Beverage,并有一个cost()方法,用来计算饮料价钱。所以,被Mocha和Whip包起来的DarkRoast对象仍然可以具有DarkRoast的一切行为,包括调用它的cost()方法。
  4. 现在,该是顾客算钱的时候了,通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱,然后再加上奶泡的价钱。

装饰我们的饮料
星巴兹装饰者类图

具体实现代码

先从Beverage类下手:

1
2
3
4
5
6
7
 abstract class Beverage{
String description="Unknown Beverage";
public String getDescription(){
return description;
}
public abstract double cost();
}

让我们也来实现Condiment(调料)抽象类,也就是装饰者类:

1
2
3
4
5
6
//首先必须让CondimentDecorator能够取代Beverage,所以将CondimentDecorator扩展自Beverage
abstract class CondimentDecorator extends Beverage{
//所有的调料装饰者都必须重新实现getDescription方法,稍后我们会解释
@Override
public abstract String getDescription();
}

写饮料的代码
现在,已经有了基类,让我们开始实现一些饮料吧,先从浓缩咖啡(Espresso)开始,别忘了,我们需要为具体的饮料设置描述,而且还必须实现cost()方法。

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
//首先,让Espresso扩展自Beverage类,因为Espresso是一种饮料。
class Espresso extends Beverage{
//为了要设置饮料的描述,我们写了一个构造器,description实例变量继承自Beverage
public Espresso(){
description="Espresso";
}
//最后,需要计算Espresso的价钱,现在不需要管调料的价钱,直接把Espresso的价格返回即可
@Override
public double cost() {
return 1.99;
}
}

class HouseBlend extends Beverage{
public HouseBlend(){
description="House Blend Coffee";
}
@Override
public double cost() {
return 0.89;
}
}

class DarkRoast extends Beverage{
public DarkRoast(){
description="DarkRoast";
}
@Override
public double cost() {
return 0.99;
}
}

class Decaf extends Beverage{
public Decaf(){
description="Decaf";
}
@Override
public double cost() {
return 1.05;
}
}

写调料代码
如果你回头看看装饰者模式的类图,将发现我们已经完成了抽象组件(Beverage),有了具体组件(HouseBlend),也有了抽象装饰者(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
48
49
50
51
52
53
54
//摩卡是一个装饰者,所以让它扩展自CondimentDecorator。别忘了,CondimentDecorator扩展自Beverage
class Mocha extends CondimentDecorator{

Beverage beverage;
public Mocha(Beverage beverage){
this.beverage=beverage;
}
//返回加入调料后的描述
@Override
public String getDescription() {
return beverage.getDescription()+",Mocha";
}
//返回装饰后的价格
@Override
public double cost() {
return 0.20+beverage.cost();
}
}

class Soy extends CondimentDecorator{

Beverage beverage;
public Soy(Beverage beverage){
this.beverage=beverage;
}

@Override
public String getDescription() {
return beverage.getDescription()+",Soy";
}

@Override
public double cost() {
return 0.15+beverage.cost();
}
}

class Whip extends CondimentDecorator{

Beverage beverage;
public Whip(Beverage beverage){
this.beverage=beverage;
}

@Override
public String getDescription() {
return beverage.getDescription()+",Whip";
}

@Override
public double cost() {
return 0.10+beverage.cost();
}
}

这里用来下订单的一些测试代码:

1
2
3
4
5
6
7
8
9
class StarbuzzCoffee{
public static void main(String arg[]){
Beverage beverage=new Espresso();
beverage=new Mocha(beverage);
beverage=new Whip(beverage);

System.out.println(beverage.getDescription()+"$"+beverage.cost());
}
}

装饰者模式的特点:

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

总结定义

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

类图框架:

装饰者类图

实际项目中的应用

真实世界的装饰者:Java I/O
java.io包内的类太多,如果你已经知道装饰者模式,这些I/O相关的类对你来说应该更有意义,因为其中许多类都是装饰者。下面是一个典型的对象集合:
装饰者I/O

装饰java.io类
装饰java.io类

编写自己的Java I/O装饰者
编写一个装饰者,把输入流内的所有大写字符转换成小写.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LowerCaseInputStream extends FilterInputStream{

protected LowerCaseInputStream(InputStream in) {
super(in);
}

@Override
public int read() throws IOException {
int c=super.read();
return (c==-1?c:Character.toLowerCase(c));
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
int result=super.read(b,off,len);
for(int i=off;i<off+result;i++){
b[i]= (byte) Character.toLowerCase(b[i]);
}
return result;
}
}

装饰者有能力为设计注入弹性,但是也有一些“缺点”:利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用此API程序员的困扰。

但是,现在你已经了解了装饰者的工作原理,以后当使用别人的大量装饰的API时,就可以很容易地辨别出他们的装饰者类是如何组织的,以便用包装方式取得想要的行为。

拿Java I/O库来说,在人们第一次接触的时候无法轻易地理解,但是认识到这些类都是用来包装InputStream的,一切就简单多了。

Fork me on GitHub