0%

设计模式学习之-03装饰模式(The Decorator Pattern)

1. 一个咖啡店的需求

一个咖啡店需要升级他们的系统,以满足饮料订单的需求。我们首先设计了一个类结构:

Beverage类是一个抽象类,其中cost是一个抽象方法,每个子类需要实现这个方法,以完成自己的定价。description是饮料的描述信息。

这样的设计初看上去是没有问题的,但是,顾客的需求是多变的,顾客可能会要求向饮料中加入一些其他的调味品,例如牛奶、可可等。咖啡店需要对这些添加的调味品收取一定的费用。于是,类图变成的下面这样。

无疑,这是一个糟糕的设计,为每一种添加不同调味品的饮料单独生成一个类,造成了类数量随调味品数量增加而指数扩展的现象。可以想象,如果某一个调味品的价格发生变化,那么对类进行修改将是一个灾难事件。

1.1. 让超类管理调味品价格怎么样?

首先我们来尝试让超类负责管理调味品价格,看结果是怎么样呢?

我们把Beverage类设计成这样,那么一个饮料如果需要添加milk,在下单时调用hasMilk()函数就可以加上milk的价格,milk的价格发生变化时,调用setMilk()
函数即可。

这样的设计比一开始的设计要好,但是仍然存在问题:

  1. 如果购买了新的调味品,就需要修改Beverage类
  2. 有些饮料可能并不需要这些调味品,例如茶(这里的情况和第一章的情况类似,并不是所有的鸭子都会飞)
  3. 如果顾客加了双份的milk怎么办呢?

2. 开闭原则(The Open-Closed Principle)

Classes should be open for extension, but closed for modification.

类应该对扩展开放,对修改关闭。

我们的目标是在不改变原有代码的前提下,让类可以方便的扩展以拥有新的行为。

需要注意的是,在任何地方都使用开闭原则是浪费的和不必要的,它会导致复杂和那一理解的代码。

3. 装饰模式

3.1. 如何操作

我们会使用Beverage并且在运行时使用调味品对其进行装饰。例如,顾客想要一杯加Mocha和Whip的DarkRoast,我们会这样做:

  1. 调用DarkRoast类,产生一个DarkRoast对象
  2. 用Mocha对象装饰它
  3. 用Whip对象装饰它
  4. 调用cost()方法并依赖委托(delegation)来添加调味品的价格

可以把装饰对象想成是一个包装,我们用Mocha对象包装了DarkRoast对象,随后又用Whip对象包装。

接下来该计算价格了,在最外层的装饰上调用cost()方法,Whip将要去被委托计算它所装饰的对象的价格,它获得一个价格就会添加到Whip的总价上

通过上面的分析我们知道:

  1. 装饰器和他们所装饰的对象有相同的超类
  2. 可以使用一个或多个装饰器对对象进行装饰
  3. 由于decorator与它所装饰的对象具有相同的超类,我们可以传递一个装饰对象来代替原始(包装)对象。
  4. The decorator adds its own behavior either before and/or after delegating to the object it decorates to do the rest of the job (装饰器在委派给其装饰的对象之前和/或之后添加自己的行为,以完成其余工作。)
  5. 对象可以在任何时间被装饰,所以我们可以在运行时为对象添加任意多的装饰器

3.2. 定义

1
2
The Decorator Pattern attaches additional responsibilities to an object dynamically.
Decorators provide a flexible alternative to subclassing for extending functionality.

3.3. 使用装饰模式重新设计

3.3.1. 代码

完整代码

3.3.1.1 Beverage类

1
2
3
4
5
6
7
8
9
public abstract class Beverage {
String description = "Unkown Bervage";

public String getDescription() {
return description;
}

public abstract double cost();
}

3.3.1.2. HouseBlend类

1
2
3
4
5
6
7
8
9
10
public class HoseBlend extends Beverage{
public HoseBlend() {
description = "HoseBlend";
}

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

3.3.1.3. CondimentDecorator类

1
2
3
public abstract class CondimentDecorator extends Beverage{
public abstract String getDescription();
}

3.3.1.4. Milk类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Milk extends CondimentDecorator {
private Beverage beverage;

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

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

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

3.3.1.5. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CoffeShop {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());

Beverage beverage1 = new DarkRoast();
beverage1 = new Mocha(beverage1);
beverage1 = new Mocha(beverage1);
beverage1 = new Whip(beverage1);
System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

Beverage beverage2 = new HoseBlend();
beverage2 = new Soy(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
}
}

执行结果

1
2
3
4
5
Espresso $1.99
DarkRoast, Mocha, Mocha, Whip $1.8
HoseBlend, Soy, Mocha, Whip $1.79

Process finished with exit code 0

4. 添加需求

咖啡店设置了3中类型的杯子(小,中,大),并且调味品根据杯子的大小收费不同,例如Soy对应(小,中,大)杯的价格分别为0.1,0.2,0.3,代码应该怎么实现呢?

4.1. 代码

完整代码

我只改动了以下3个类,测试时需要打开注释的部分。

4.1.1. Beverage类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Beverage {
String description = "Unkown Bervage";
public enum Size {SMALL, MEDIUM, LARGE};
Size size = Size.SMALL;

public Size getSize() {
return size;
}

public void setSize(Size size) {
this.size = size;
}

public String getDescription() {
return description;
}

public abstract double cost();
}

在原有的代码基础上添加了

1
2
3
4
5
6
7
8
9
10
public enum Size {SMALL, MEDIUM, LARGE};
Size size = Size.SMALL;

public Size getSize() {
return size;
}

public void setSize(Size size) {
this.size = size;
}

4.1.2. HoseBlend类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HoseBlend extends Beverage{
private Map<Size, Double> price = new HashMap<Size, Double>();

public HoseBlend() {
description = "HoseBlend";
price.put(Size.SMALL, 0.1);
price.put(Size.MEDIUM, 0.2);
price.put(Size.LARGE, 0.3);
}

@Override
public double cost() {
return price.get(getSize());
}
}

4.1.3. Milk类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Milk extends CondimentDecorator {
private Beverage beverage;
private Map<Size, Double> price = new HashMap<Size, Double>();

public Milk(Beverage beverage) {
this.beverage = beverage;
setSize(beverage.getSize());

price.put(Size.SMALL, 0.11);
price.put(Size.MEDIUM, 0.15);
price.put(Size.LARGE, 0.31);
}

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

@Override
public double cost() {
return beverage.cost() + price.get(getSize());
}
}

4.1.4. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CoffeShop {
public static void main(String[] args) {
everage beverage3 = new HoseBlend();
beverage3.setSize(Beverage.Size.MEDIUM);
beverage3 = new Milk(beverage3);
System.out.println("Cup Size " + beverage3.getSize() + " " + beverage3.getDescription() + " $" + beverage3.cost());

Beverage beverage4 = new HoseBlend();
beverage4.setSize(Beverage.Size.LARGE);
beverage4 = new Milk(beverage4);
System.out.println("Cup Size " + beverage4.getSize() + " " + beverage4.getDescription() + " $" + beverage4.cost());
}
}

执行结果:

1
2
Cup Size MEDIUM HoseBlend, Milk $0.35
Cup Size LARGE HoseBlend, Milk $0.61

5. 真实工程中的装饰模式的应用

java.io 中就使用了装饰模式

InputStream是抽象类

FilterInputStream是一个抽象装饰器

5.1. 使用InputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test1 {
public static void main(String[] args) {
int c;
try {
File file = new File("test");
InputStream stream = new FileInputStream(file);
stream = new BufferedInputStream(stream);
stream = new LineInputStream(stream);

while ((c = stream.read()) >= 0) {
System.out.print((char) c);
}

stream.close();

} catch (IOException e) {
e.printStackTrace();
}
}
}

5.2. 自定义java I/O装饰器

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 LowerCaseInputStream extends FilterInputStream {
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected LowerCaseInputStream(InputStream in) {
super(in);
}

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

public int read(byte[] b, int offset, int len) throws IOException {
int result = in.read(b, offset, len);
for (int i=offset; i<offset+result; i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test2 {
public static void main(String[] args) {
int c;
try {
InputStream in = new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("test")));

while ((c = in.read()) >= 0) {
System.out.print((char)c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}