装饰模式

设计模式笔记:装饰模式 (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 接口,是“被装饰”的原始对象。它代表了最核心、最基础的功能。
    • 例如,Espresso、Decaf。
  3. **抽象装饰器 (Decorator)**:
    • 这是一个关键角色。它是一个抽象类,也实现了 Component 接口。
    • 它内部持有一个 Component 对象的引用(wrappedObject)。这是实现“包装”的核心。
    • 它将 Component 接口中的方法委托给其持有的 wrappedObject 去执行。例如,decorator.getCost() 的默认实现是 return wrappedObject.getCost()。
  4. **具体装饰器 (Concrete Decorator)**:
    • 继承自抽象装饰器 Decorator。
    • 它负责为被包装的对象添加具体的职责。
    • 它会重写父类的方法,在调用“父类”(即委托给被包装对象)的方法前后,加上自己的附加逻辑。
    • 例如,MilkDecorator 的 getCost() 方法会返回 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. 创建抽象的 Decorator 类 CoffeeDecorator,它也实现 Coffee 接口,并持有一个 Coffee 类型的成员变量。
  4. 创建具体的 ConcreteDecorator 类,如 MilkDecorator、MochaDecorator,它们继承自 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

图示解读:

  • Espresso 和 CoffeeDecorator 都实现了 Coffee 接口,这使得它们可以被客户端统一对待。
  • CoffeeDecorator 持有一个 Coffee 对象的引用,这是“包装”的关键。
  • MilkDecorator 和 MochaDecorator 继承自 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 的引用,并在每个方法调用前后添加了同步锁或检查逻辑。

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