装饰模式
装饰模式
Rif设计模式笔记:装饰模式 (Decorator Pattern)
一、一句话概括
在不改变原有对象结构的基础上,动态地为对象添加新的功能。它就像给对象穿上一层又一层的“外套”,每件外套都增加一种新能力。
二、为什么需要它 (Why - The Pain Point)
假设你正在开发一个咖啡店的点单系统。一开始,你只有几种基础咖啡:
1 | // 基础咖啡接口 |
现在,顾客要求可以加各种调料,如牛奶(Milk)、摩卡(Mocha)、豆浆(Soy)等,每种调料都有自己的价格和描述。
你会怎么做?
糟糕的方案:使用继承
你可能会想为每一种组合创建一个子类:
1 | class CoffeeWithMilk extends Coffee { ... } |
痛点:类爆炸 (Class Explosion)
这种方案的弊端是灾难性的:
- 组合数量巨大:如果只有3种咖啡和3种调料,可能的组合就有
(2^3 - 1) * 3 = 21种之多(每种咖啡可以加1-3种调料)。类的数量会随着调料的增加而呈指数级增长。 - 灵活性极差:无法在运行时动态地改变对象的行为。一杯咖啡加了什么调料,在创建对象时就已经“写死”了。
- 违反开闭原则:每增加一种新的调料,可能就需要创建一系列新的子类,系统难以维护和扩展。
核心问题:如何在不创建大量子类的情况下,灵活、动态地给对象添加职责?
三、它是什么 (What - The Solution)
装饰模式通过一种“包装”和“委托”的机制,巧妙地解决了这个问题。它允许你将对象放入一个“装饰器”对象中,从而为其添加新功能。你可以一层一层地进行包装,实现功能的任意组合。
核心思想和角色:
- **组件 (Component)**:
- 定义了被装饰对象和装饰器对象的共同接口。客户端将通过这个接口与对象进行交互。
- 在我们的例子中,就是
Coffee接口。
- **具体组件 (Concrete Component)**:
- 实现了
Component接口,是“被装饰”的原始对象。它代表了最核心、最基础的功能。 - 例如,
Espresso、Decaf。
- 实现了
- **抽象装饰器 (Decorator)**:
- 这是一个关键角色。它是一个抽象类,也实现了
Component接口。 - 它内部持有一个
Component对象的引用(wrappedObject)。这是实现“包装”的核心。 - 它将
Component接口中的方法委托给其持有的wrappedObject去执行。例如,decorator.getCost()的默认实现是return wrappedObject.getCost()。
- 这是一个关键角色。它是一个抽象类,也实现了
- **具体装饰器 (Concrete Decorator)**:
- 继承自抽象装饰器
Decorator。 - 它负责为被包装的对象添加具体的职责。
- 它会重写父类的方法,在调用“父类”(即委托给被包装对象)的方法前后,加上自己的附加逻辑。
- 例如,
MilkDecorator的getCost()方法会返回super.getCost() + 0.5(即被包装对象的成本 + 牛奶的成本)。
- 继承自抽象装饰器
C++ 背景对比 & Java 关键点:
- 与C++的联系:这个模式和C++的模板元编程或者通过嵌套类封装有相似之处,但Java的实现方式更依赖于对象组合和继承。
- Java 的实现精髓:
- 组合优于继承:装饰模式是“组合优于继承”原则的典范。它通过组合(包装)来扩展功能,而不是通过继承。
- 递归结构:由于装饰器和被装饰对象都实现了同一个接口,装饰器可以包装另一个装饰器,形成一个递归的调用链,例如
new Milk(new Mocha(new Espresso()))。 - 单一职责原则:每个具体装饰器只负责添加一种功能,职责清晰。
四、怎么做 (How - The Blueprint)
基本步骤(以咖啡店为例):
- 创建
Component接口Coffee,定义getCost()和getDescription()方法。 - 创建
ConcreteComponent类,如Espresso,实现Coffee接口。 - 创建抽象的
Decorator类CoffeeDecorator,它也实现Coffee接口,并持有一个Coffee类型的成员变量。 - 创建具体的
ConcreteDecorator类,如MilkDecorator、MochaDecorator,它们继承自CoffeeDecorator。在构造函数中接收一个Coffee对象(被包装的对象),并重写getCost()和getDescription()方法,在其中添加自己的逻辑。 - 客户端通过层层嵌套这些装饰器来动态地构建一杯定制咖啡。
Java 示例代码
1 | // 1. 组件 (Component) |
五、UML 类图
下面是上述例子的 PlantUML 代码。
1 | @startuml |
图示解读:
Espresso和CoffeeDecorator都实现了Coffee接口,这使得它们可以被客户端统一对待。CoffeeDecorator持有一个Coffee对象的引用,这是“包装”的关键。MilkDecorator和MochaDecorator继承自CoffeeDecorator,实现了具体的装饰逻辑。
六、优缺点分析
优点 (Pros):
- 高度的灵活性:可以在运行时动态地添加或删除功能,比静态继承灵活得多。
- 避免类爆炸:可以用少量简单的类组合出大量的功能,有效控制了类的数量。
- 遵循开闭原则:可以为现有类添加新功能,而无需修改其源代码。
- 遵循单一职责原则:每个装饰器类只负责一项职责,功能内聚。
缺点 (Cons):
- 产生大量小对象:使用装饰模式会创建许多细粒度的对象(每个装饰器都是一个对象),如果过度使用,可能会增加系统的复杂性。
- 调试困难:由于对象被层层包装,查找某个特定功能的来源可能会变得困难,调用栈会很深。
- 顺序敏感:装饰器的应用顺序可能会影响最终结果和行为,需要注意。
七、在 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();- Component:
java.util.Collections中的同步/不可修改包装器:Collections.synchronizedList(new ArrayList<>())Collections.unmodifiableList(new ArrayList<>())
这些方法返回一个实现了List接口的包装类,它持有原始List的引用,并在每个方法调用前后添加了同步锁或检查逻辑。
装饰模式是一个非常强大的工具,用于在不破坏封装的前提下扩展对象功能。准备好后,我们就可以继续学习适配器模式了。