11|装饰器:通过包装逐层增加行为
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.decorator。
系列导航:Java 25 设计模式学习指南
文本展示需要去除首尾空白,有些页面还需要外层方括号。若用继承表示“原文本”“去空白文本”“带框文本”“去空白且带框文本”,组合一多,子类就会迅速增长。
装饰器把每一种增强独立成一个对象,并允许它包装同一接口的另一个对象。调用者仍然只执行 read,却可以在装配处决定处理顺序。
从问题提炼设计意图
装饰器通过组合包装组件,保持相同接口并在调用前后增加行为,使增强能够在对象层面动态组合。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
TextSource |
组件接口 |
PlainText |
提供原始文本的具体组件 |
TextDecorator |
保存并委托内层组件的装饰基类 |
TrimmedText、FramedText |
两种可组合增强 |
classDiagram
TextSource <|.. PlainText
TextSource <|.. TextDecorator
TextDecorator --> TextSource : wraps
TextDecorator <|-- TrimmedText
TextDecorator <|-- FramedText
代码思路:变化应该落在哪个对象上
TextDecorator 自己实现 TextSource,同时又持有一个 TextSource。这是可递归包装的关键:被包装者可以是 PlainText,也可以是另一个装饰器。
TrimmedText 先调用 super.read 取得内层结果,再 strip;FramedText 则给内层结果加方括号。外层调用会向内逐层委托,结果再从内向外逐层加工。装配顺序定义了运算顺序。
以“ Java ”为输入,先去空白再加框得到“[Java]”;先加框再去空白得到“[ Java ]”,因为空格已经在方括号内部。这个反例说明装饰组合通常不满足交换律,测试应明确关心顺序。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.decorator,源码目录为 src/main/java/com/hanserwei/patterns/decorator/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
TextDecorator.java:
package com.hanserwei.patterns.decorator;
import java.util.Objects;
/** 把调用委托给被包装对象的装饰基类. */
public abstract class TextDecorator implements TextSource {
/** 被包装的文本源,可以是另一个装饰器. */
private final TextSource source;
/** 注入被包装组件. */
protected TextDecorator(TextSource source) {
this.source = Objects.requireNonNull(source, "source");
}
/** 读取内层文本,供具体装饰器增强. */
@Override
public String read() {
return source.read();
}
}
TrimmedText.java:
package com.hanserwei.patterns.decorator;
/** 可组合的文本增强器. */
public final class TrimmedText extends TextDecorator {
/** 包装文本源并保留同一读取接口. */
public TrimmedText(TextSource source) {
super(source);
}
/** 先读取内层结果,再应用本层转换. */
@Override
public String read() {
return super.read().strip();
}
}
FramedText.java:
package com.hanserwei.patterns.decorator;
/** 可组合的文本增强器. */
public final class FramedText extends TextDecorator {
/** 包装文本源并保留同一读取接口. */
public FramedText(TextSource source) {
super(source);
}
/** 先读取内层结果,再应用本层转换. */
@Override
public String read() {
return "[" + super.read() + "]";
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.decorator;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
TextSource source = new FramedText(new TrimmedText(new PlainText(" Java ")));
System.out.println(source.read());
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runDecorator
./gradlew test --tests 'com.hanserwei.patterns.decorator.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
[Java]
用测试确认模式的行为
交换装饰顺序得到不同结果,原文本保持不变;单层包装也符合 TextSource 契约。
对应测试位于 src/test/java/com/hanserwei/patterns/decorator/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 流、文本处理和消息处理中按需叠加转换能力。
- 给组件加日志、计量、压缩等横切行为,同时维持调用接口。
- 运行时根据配置选择增强组合,避免组合子类爆炸。
适用边界与容易踩的坑
增强不能破坏组件契约。如果内层承诺一次性流语义,装饰器反复读取它就可能出错;如果内层需要关闭资源,包装层必须清楚谁负责关闭、关闭顺序如何。本例使用纯字符串,故意避开资源生命周期,但真实实现不能忽略它。
包装层太多会让调试栈和行为顺序难以理解。装配代码应集中、可读,并测试关键组合;不是层数越多设计越先进。
本例的装饰器不改变 PlainText,因而多个组合可以共享同一个原始文本对象。如果包装的是可变组件,共享会产生不同生命周期下的状态关联,需要重新判断所有权。
与相近模式比较
代理也可能保持相同接口并持有目标对象,但重点是控制访问,例如缓存或权限;装饰器的重点是给调用方组合额外能力。外形相似时,应从意图和客户端的装配方式判断。
动手练习
增加 UppercaseText,并列举三种装配顺序的预期结果。再把增强换成“截断到固定长度”,寻找两个不满足交换律的组合,解释为什么测试必须覆盖顺序而非只单测每个装饰器。
系列导航:Java 25 设计模式学习指南