《Head First 设计模式》笔记3

装饰者模式(Decorate)

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

设计原则四:类应该对扩展开放,对修改关闭。

如果使用过 Python,应该听过装饰器,虽然概念有点不同,但都是通过动态添加的方式给对象扩展功能。

栗子

星巴克的订单系统系统中有个饮料抽象类 Beverage,店内的饮料都必须继承该类:

1
2
3
4
5
6
7
8
9
10
11
abstract class Beverage {
// 饮料描述,比如咖啡、牛奶
protected String description;

public String getDescription() {
return this.description;
}

// 饮料价钱
public abstract float cost();
}

其中一些饮料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Coffee extends Beverage {
public Coffee() {
description = "Coffee";
}

@Override
public float cost() {
return 3.2f;
}
}

class Milk extends Beverage {
public Milk() {
description = "Milk";
}

@Override
public float cost() {
return 4.6f;
}
}

// ...

提需求

现在,客户想要在买的饮料里加点调味料,比如买了杯咖啡,要加点牛奶、豆浆、巧克力等。这就需要订单系统在统计饮料价格时加上调味料的价格。

错误示范1

每种可能的调味料的饮料都新建一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CoffeeWithMilk extends Beverage {
@Override
public float cost() {
return 7.9f;
}
}

class MilkWithCoffee extends Beverage {
@Override
public float cost() {
return 8.2f;
}
}

// ....

很明显,调味料有很多,饮料也有很多,如果只加一种调味料,那搭配起来也有非常多的可能,如果都新建一个类,那么就会造成“类爆炸”。而且这些调味料的价格如果发生变化,就要将涉及的饮料都修改一遍,严重违反了设计原则中的“将变化与不变化的代码分开”。

错误示范2

既然违反了变化的原则,那么尝试把这些变化的调味料都放在饮料抽象类 Beverage 中?

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
abstract class Beverage {
protected String description;
// 是否添加了某些调味料
protected boolean milk;
protected boolean coffee;

public String getDescription() {
return this.description;
}

// 计算好添加的调味料价格,让子类直接调用
public float cost() {
float sum = 0;
if (hasCoffee()) {
sum += 3.2f;
}
if (hasMilk()) {
sum += 4.6f;
}
return sum;
}

protected boolean hasCoffee() {
return this.coffee;
}

protected void setCoffee(boolean coffee) {
this.coffee = coffee;
}

protected boolean hasMilk() {
return this.milk;
}

protected void setMilk(boolean milk) {
this.milk = milk;
}
}

饮料只要设置好添加的调味料,最后计算下自身花费就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Coffee extends Beverage {
@Override
public float cost() {
return super.cost() + 3.2f;
}
}

class Milk extends Beverage {
@Override
public float cost() {
return super.cost() + 4.6f;
}
}

调用:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Coffee coffee = new Coffee();
// 加点牛奶
coffee.setMilk(true);
System.out.println(coffee.cost());
}
}

现在没有了“类爆炸”,就算调料价格发生变化,只要修改下抽象类就好了,但是真的没问题了吗?

有问题,而且挺多的:

  1. 如果出现新的调味料,就需要在抽象类添加新的布尔值成员,cost 方法也需要添加新的判断,还要分别新增一个 setter 和一个 getter,如果是删除一个调味料呢?
  2. 对于有些饮料来说,某些调味料并不能加(会拉肚子的!),比如冰红茶 + 牛奶?
  3. 如果客户要一杯咖啡,加两份牛奶呢?

满足需求

不改变原有的饮料抽象和具体的饮料:

1
2
3
4
5
6
7
8
9
abstract class Beverage {
protected String description;

public String getDescription() {
return this.description;
}

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

@Override
public float cost() {
return 3.2f;
}
}

添加调味料(condiment)装饰者:

1
2
3
4
abstract class CondimentDecorator extends Beverage {
@Override
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
28
29
30
31
32
33
34
35
class Milk extends CondimentDecorator {
Beverage beverage;

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

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

@Override
public float cost() {
return .50f + beverage.cost();
}
}

class Mocha extends CondimentDecorator {
Beverage beverage;

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

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

@Override
public float cost() {
return .20f + beverage.cost();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Beverage coffee1 = new Coffee();
// 不需要调味料
System.out.println(coffee1.getDescription() + " $" + coffee1.cost());

Beverage coffee2 = new Coffee();
// 加牛奶和摩卡
coffee2 = new Milk(coffee2);
coffee2 = new Mocha(coffee2);
System.out.println(coffee2.getDescription() + " $" + coffee2.cost());

Beverage coffee3 = new Coffee();
// 加3份牛奶和2份摩卡
coffee3 = new Milk(coffee3);
coffee3 = new Milk(coffee3);
coffee3 = new Milk(coffee3);
coffee3 = new Mocha(coffee3);
coffee3 = new Mocha(coffee3);
System.out.println(coffee3.getDescription() + " $" + coffee3.cost());
}
}

输出:
Coffee $3.2
Coffee + milk + mocha $3.9
Coffee + milk + milk + milk + mocha + mocha $5.0999994

Author

Zoctan

Posted on

2018-03-30

Updated on

2023-03-14

Licensed under