装饰模式

设计模式笔记:装饰模式 (Decorator Pattern)

一、一句话概括

不改变原有对象结构的基础上,动态地为对象添加新的功能。它就像给对象穿上一层又一层的“外套”,每件外套都增加一种新能力。

二、为什么需要它 (Why - The Pain Point)

假设你正在开发一个咖啡店的点单系统。一开始,你只有几种基础咖啡:

1
2
3
4
5
6
7
8
// 基础咖啡接口
interface Coffee {
double getCost();
String getDescription();
}

class Espresso implements Coffee { /* ... */ }
class Decaf implements Coffee { /* ... */ }

现在,顾客要求可以加各种调料,如牛奶(Milk)、摩卡(Mocha)、豆浆(Soy)等,每种调料都有自己的价格和描述。

你会怎么做?

糟糕的方案:使用继承

你可能会想为每一种组合创建一个子类:

1
2
3
4
5
class CoffeeWithMilk extends Coffee { ... }
class CoffeeWithMocha extends Coffee { ... }
class EspressoWithMilk extends Espresso { ... }
class EspressoWithMilkAndMocha extends Espresso { ... }
// ... 无穷无尽

痛点:类爆炸 (Class Explosion)

这种方案的弊端是灾难性的:

  1. 组合数量巨大:如果只有3种咖啡和3种调料,可能的组合就有 (2^3 - 1) * 3 = 21 种之多(每种咖啡可以加1-3种调料)。类的数量会随着调料的增加而呈指数级增长。
  2. 灵活性极差:无法在运行时动态地改变对象的行为。一杯咖啡加了什么调料,在创建对象时就已经“写死”了。
  3. 违反开闭原则:每增加一种新的调料,可能就需要创建一系列新的子类,系统难以维护和扩展。

核心问题:如何在不创建大量子类的情况下,灵活、动态地给对象添加职责?

三、它是什么 (What - The Solution)

装饰模式通过一种“包装”和“委托”的机制,巧妙地解决了这个问题。它允许你将对象放入一个“装饰器”对象中,从而为其添加新功能。你可以一层一层地进行包装,实现功能的任意组合。

核心思想和角色:

  1. **组件 (Component)**:
    • 定义了被装饰对象和装饰器对象的共同接口。客户端将通过这个接口与对象进行交互。
    • 在我们的例子中,就是 Coffee 接口。
  2. **具体组件 (Concrete Component)**:
    • 实现了 Component 接口,是“被装饰”的原始对象。它代表了最核心、最基础的功能。
    • 例如,EspressoDecaf
  3. **抽象装饰器 (Decorator)**:
    • 这是一个关键角色。它是一个抽象类,也实现了 Component 接口。
    • 它内部持有一个 Component 对象的引用(wrappedObject。这是实现“包装”的核心。
    • 它将 Component 接口中的方法委托给其持有的 wrappedObject 去执行。例如,decorator.getCost() 的默认实现是 return wrappedObject.getCost()
  4. **具体装饰器 (Concrete Decorator)**:
    • 继承自抽象装饰器 Decorator
    • 它负责为被包装的对象添加具体的职责
    • 它会重写父类的方法,在调用“父类”(即委托给被包装对象)的方法前后,加上自己的附加逻辑。
    • 例如,MilkDecoratorgetCost() 方法会返回 super.getCost() + 0.5(即被包装对象的成本 + 牛奶的成本)。

C++ 背景对比 & Java 关键点:

  • 与C++的联系:这个模式和C++的模板元编程或者通过嵌套类封装有相似之处,但Java的实现方式更依赖于对象组合继承
  • Java 的实现精髓
    • 组合优于继承:装饰模式是“组合优于继承”原则的典范。它通过组合(包装)来扩展功能,而不是通过继承。
    • 递归结构:由于装饰器和被装饰对象都实现了同一个接口,装饰器可以包装另一个装饰器,形成一个递归的调用链,例如 new Milk(new Mocha(new Espresso()))
    • 单一职责原则:每个具体装饰器只负责添加一种功能,职责清晰。

四、怎么做 (How - The Blueprint)

基本步骤(以咖啡店为例):

  1. 创建 Component 接口 Coffee,定义 getCost()getDescription() 方法。
  2. 创建 ConcreteComponent 类,如 Espresso,实现 Coffee 接口。
  3. 创建抽象的 DecoratorCoffeeDecorator,它也实现 Coffee 接口,并持有一个 Coffee 类型的成员变量。
  4. 创建具体的 ConcreteDecorator 类,如 MilkDecoratorMochaDecorator,它们继承自 CoffeeDecorator。在构造函数中接收一个 Coffee 对象(被包装的对象),并重写 getCost()getDescription() 方法,在其中添加自己的逻辑。
  5. 客户端通过层层嵌套这些装饰器来动态地构建一杯定制咖啡。

Java 示例代码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 1. 组件 (Component)
interface Coffee {
double getCost();
String getDescription();
}

// 2. 具体组件 (Concrete Component)
class Espresso implements Coffee {
@Override
public double getCost() {
return 1.99;
}

@Override
public String getDescription() {
return "Espresso";
}
}

class Decaf implements Coffee {
@Override
public double getCost() {
return 2.50;
}

@Override
public String getDescription() {
return "Decaf Coffee";
}
}

// 3. 抽象装饰器 (Decorator)
abstract class CoffeeDecorator implements Coffee {
// 持有被装饰者的引用
protected Coffee wrappedCoffee;

public CoffeeDecorator(Coffee coffee) {
this.wrappedCoffee = coffee;
}

// 默认实现是委托给被包装的对象
@Override
public double getCost() {
return wrappedCoffee.getCost();
}

@Override
public String getDescription() {
return wrappedCoffee.getDescription();
}
}

// 4. 具体装饰器 (Concrete Decorator)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public double getCost() {
// 在被包装对象的基础上增加自己的成本
return super.getCost() + 0.50;
}

@Override
public String getDescription() {
// 在被包装对象的描述上增加自己的描述
return super.getDescription() + ", with Milk";
}
}

class MochaDecorator extends CoffeeDecorator {
public MochaDecorator(Coffee coffee) {
super(coffee);
}

@Override
public double getCost() {
return super.getCost() + 0.75;
}

@Override
public String getDescription() {
return super.getDescription() + ", with Mocha";
}
}

// 5. 客户端
public class DecoratorPatternDemo {
public static void main(String[] args) {
// 点一杯浓缩咖啡,什么都不加
Coffee simpleCoffee = new Espresso();
System.out.println("Item: " + simpleCoffee.getDescription() + ", Cost: $" + simpleCoffee.getCost());

System.out.println("---------------------------------");

// 点一杯低因咖啡,加牛奶
Coffee decafWithMilk = new MilkDecorator(new Decaf());
System.out.println("Item: " + decafWithMilk.getDescription() + ", Cost: $" + decafWithMilk.getCost());

System.out.println("---------------------------------");

// 点一杯浓缩咖啡,加双份摩卡和一份牛奶 (装饰器可以嵌套)
Coffee superFancyCoffee = new MilkDecorator(new MochaDecorator(new MochaDecorator(new Espresso())));
System.out.println("Item: " + superFancyCoffee.getDescription() + ", Cost: $" + superFancyCoffee.getCost());
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码。

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

interface Coffee {
+ double getCost()
+ String getDescription()
}

class Espresso implements Coffee
class Decaf implements Coffee

abstract class CoffeeDecorator implements Coffee {
# Coffee wrappedCoffee
+ CoffeeDecorator(Coffee coffee)
}

class MilkDecorator extends CoffeeDecorator {
+ double getCost()
+ String getDescription()
}

class MochaDecorator extends CoffeeDecorator {
+ double getCost()
+ String getDescription()
}

' 关系
' 装饰器 "有一个" 组件 (聚合关系)
CoffeeDecorator o-- "1" Coffee

class DecoratorPatternDemo {}
DecoratorPatternDemo ..> Coffee : uses

@enduml

图示解读:

  • EspressoCoffeeDecorator 都实现了 Coffee 接口,这使得它们可以被客户端统一对待。
  • CoffeeDecorator 持有一个 Coffee 对象的引用,这是“包装”的关键。
  • MilkDecoratorMochaDecorator 继承自 CoffeeDecorator,实现了具体的装饰逻辑。

六、优缺点分析

优点 (Pros):

  1. 高度的灵活性:可以在运行时动态地添加或删除功能,比静态继承灵活得多。
  2. 避免类爆炸:可以用少量简单的类组合出大量的功能,有效控制了类的数量。
  3. 遵循开闭原则:可以为现有类添加新功能,而无需修改其源代码。
  4. 遵循单一职责原则:每个装饰器类只负责一项职责,功能内聚。

缺点 (Cons):

  1. 产生大量小对象:使用装饰模式会创建许多细粒度的对象(每个装饰器都是一个对象),如果过度使用,可能会增加系统的复杂性。
  2. 调试困难:由于对象被层层包装,查找某个特定功能的来源可能会变得困难,调用栈会很深。
  3. 顺序敏感:装饰器的应用顺序可能会影响最终结果和行为,需要注意。

七、在 Java 中的应用

  • Java I/O 类库: 这是装饰模式在JDK中最经典、最完美的体现。

    • Component: InputStream / Reader
    • ConcreteComponent: FileInputStream / FileReader (从数据源读取原始字节/字符)
    • Decorator: FilterInputStream / FilterReader (抽象装饰器)
    • ConcreteDecorator:
      • BufferedInputStream: 增加缓冲功能,提高读取效率。
      • DataInputStream: 增加读取基本数据类型(readInt(), readDouble())的功能。
      • GZIPInputStream: 增加解压缩功能。

    你可以像这样组合它们:

    1
    2
    3
    4
    5
    6
    7
    // 逐层包装,为文件输入流添加缓冲和数据读取功能
    DataInputStream dis = new DataInputStream(
    new BufferedInputStream(
    new FileInputStream("myFile.dat")
    )
    );
    int myInt = dis.readInt();
  • java.util.Collections 中的同步/不可修改包装器:

    • Collections.synchronizedList(new ArrayList<>())
    • Collections.unmodifiableList(new ArrayList<>())
      这些方法返回一个实现了 List 接口的包装类,它持有原始 List 的引用,并在每个方法调用前后添加了同步锁或检查逻辑。

装饰模式是一个非常强大的工具,用于在不破坏封装的前提下扩展对象功能。准备好后,我们就可以继续学习适配器模式了。