【设计模式】装饰模式(一)实战:从计费系统的"动态加功能"说起
【设计模式】装饰模式(三):从装饰模式看 Java 与面向对象设计(原理篇)-CSDN博客
引言
装饰模式不是教科书玩具,我们天天跟它打交道------读文件时 new BufferedInputStream(new FileInputStream("a.txt")),写 Web 时 Filter 里 new TrimParameterRequestWrapper(request),用 MyBatis 时二级缓存自动生效------但我从未把它们跟"装饰模式"这四个字联系起来。在这篇文章我们将深入我们熟知的框架中学习装饰模式,层层拆解,学习优秀框架如何"穿搭"。
一、Java I/O:教科书级的装饰模式现场
小明去东北,自己决定穿几件衣服------想套就套,想脱就脱,每件各管各的保暖。
Java I/O 是装饰模式在 JDK 里最经典的现场。
1.1 类图 + 角色对照

| 装饰模式角色 | Java I/O 里的对应 |
|---|---|
| 抽象组件 | InputStream |
| 具体组件("人") | FileInputStream、ByteArrayInputStream |
| 抽象装饰器("衣架") | FilterInputStream |
| 具体装饰器("衣服") | BufferedInputStream、DataInputStream |
1.2 FilterInputStream 是空壳吗?
看源码,FilterInputStream 里几乎所有方法都是一行 return in.xxx()。它自己不干正事,那存在的意义是什么?
工程层面的复用,不是语法层面的必须。
它干了三件事:
- 统一声明
protected InputStream in字段------所有具体装饰器不用各自重复声明 - 提供默认委托实现------子类只重写需要改的方法,其余直接继承默认行为
- 给所有装饰器一个共同父类------方便类型判断和统一管理
如果你把 FilterInputStream 删掉,让 BufferedInputStream 直接继承 InputStream------透明化照样成立 ,只是每个装饰器都要自己写 in 字段和构造器,样板代码翻倍。
1.3 调用链
java
InputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.txt")));
in.read();
实际执行顺序(从外到内):
java
DataInputStream.read()
→ 需要读 4 个字节拼 int
→ BufferedInputStream.read()
→ 查 buf,有数据直接返回
→ buf 空了,调 FileInputStream.read(byte[])
→ 操作系统真正读磁盘
每一层只干自己的事:DataInputStream 管类型解析,BufferedInputStream 管批量缓冲,FileInputStream 管真正读文件。调用方只管 read(),不知道里面套了几层。
1.4 小结
Java I/O 是装饰模式的标准标杆:抽象装饰器复用结构,具体装饰器各管各的附加职责,调用方只认抽象组件类型。
二、Spring HttpServletRequestWrapper:半透明的妥协
小明戴了顶帽子,平时别人跟他说话正常聊(透明),但有人想摸帽子上的装饰就得扒开认出"哦你戴了帽"(强转/半透明)。
2.1 场景引入
产品经理说:所有接口提交过来的参数,自动去掉前后空格,Controller 不用自己 trim。
你第一反应可能是 AOP?但 AOP 拿不到 HttpServletRequest 里的原始参数。直接改 Tomcat 源码?别闹。
装饰模式:写一个 HttpServletRequestWrapper 子类,重写 getParameter(),在里面 trim。

2.2 从零写一个 Wrapper
java
public class TrimParameterRequestWrapper extends HttpServletRequestWrapper {
public TrimParameterRequestWrapper(HttpServletRequest request) {
super(request); // 把原始 request 传给父类,存在 this.request 字段里
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name); // 先拿原始值
return value != null ? value.trim() : null; // 再 trim
}
}
2.3 Filter 里套上去
java
@Component
public class TrimFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedRequest =
new TrimParameterRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response); // 传的是装饰后的对象
}
}
chain.doFilter(wrappedRequest, ...) 把装饰后的 request 沿 Filter 链往后传,最终到达 Controller。
Controller 里:
java
@PostMapping("/user")
public String createUser(HttpServletRequest request) {
String name = request.getParameter("name");
// 用户提交 " 张三 "
// request 实际类型是 TrimParameterRequestWrapper
// getParameter() 被重写了,自动 trim
// name 直接就是 "张三"
return "hello " + name;
}
**Controller 一行没改,但行为变了。** 这就是透明化------跟 Java I/O 一模一样的道理。
2.4 半透明
现在想加个批量方法,一次性返回所有参数(已 trim 的):
java
public Map<String, String> getAllParamsTrimmed() {
Map<String, String> result = new HashMap<>();
Enumeration<String> names = getParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
result.put(name, getParameter(name));
}
return result;
}
Controller 里想用:
java
// request 声明类型是 HttpServletRequest
// getAllParamsTrimmed() 是 Wrapper 特有方法,接口里没有
// request.getAllParamsTrimmed(); // 编译报错
// 必须强转:
TrimParameterRequestWrapper wrapper = (TrimParameterRequestWrapper) request;
wrapper.getAllParamsTrimmed();
透明性破了。 Controller 原本只认 HttpServletRequest,现在必须知道"这是个 TrimParameterRequestWrapper"才能调特有方法。这就是半透明------大部分方法透明用,特有方法必须强转。
2.5 为什么工程上能接受
| 方案 | 透明性 | 代价 |
|---|---|---|
| 纯装饰(只重写接口方法) | ✅ 完全透明 | 不能加新功能 |
| 半透明(加特有方法) | ⚠️ 部分透明 | 少数地方需要强转 |
| 直接改核心类 | ❌ 破坏开闭原则 | 改容器代码,升级就炸 |
HttpServletRequest 是 Servlet 规范定义的接口,你改不了。半透明是三者中最务实的选择------大部分场景享受透明化的好处,少数场景付出强转的代价,两害相权取其轻。
2.6 小结
Spring 的 HttpServletRequestWrapper 展示了装饰模式在真实 Web 框架里的样子:**大部分时候完全透明,偶尔为了扩展特有功能破一下透明,这是工程实践里可接受的代价。** 它验证了第二篇说的------透明化不是绝对的,当你需要新增接口之外的方法时,半透明是唯一的出路。
三、MyBatis 缓存:装饰模式的工业化生产
小明上台唱戏,戏服按角色要求一层层穿,顺序不能乱------盔头戴错位了上台就是笑话。
3.1 类图

3.2 特点速览
| 特点 | 说明 |
|---|---|
| 无抽象装饰器 | 每个装饰器直接实现 Cache 接口,自己声明 delegate 字段。抽象装饰器是工程最佳实践,不是语法必须 |
| 每层只干一件事 | 日志、同步、LRU、定时清理、序列化------各管各的,互不耦合 |
| 框架自动组装 | 根据配置决定套几层、什么顺序,不需要手动 new |
| 顺序敏感 | 序列化须在最外层(先序列化再存),LRU 须紧贴 PerpetualCache(直接管 HashMap 大小),反了功能就废 |
3.3 小结
MyBatis 缓存是装饰模式的"工业化生产"版本------没有抽象装饰器,每个装饰器只干一件事,框架根据配置自动组装,调用方只管 get 和 put,完全不知道里面套了几层。它证明了装饰模式不仅能手动用,还能被框架自动化地玩出花来。
四、三框架横向对比
| 对比维度 | Java I/O | Spring Wrapper | MyBatis 缓存 |
|---|---|---|---|
| 抽象装饰器 | 有 | 有 | 无,直接实现接口 |
| 组装方式 | 手动 new |
Filter 里手动 new |
框架按配置自动组装 |
| 透明性 | 完全透明 | 半透明(特有方法需强转) | 完全透明 |
| 装饰器数量 | 不固定,按需叠加 | 通常 1~2 层,也可更多 | 0~6+ 层,配置驱动 |
| 顺序是否敏感 | 部分敏感(DataInputStream 须在外层) |
一般不敏感(Wrapper 间通常互不影响) | 敏感(序列化最外、LRU 紧贴核心) |
五、什么时候该想到装饰模式
5.1 3 秒识别法
看到代码里有这些信号,就该想到装饰模式:
- 构造器接收一个同类对象
- 类内部持有同类型引用
- 方法体里干了自己的事,同时委托给内部对象(顺序可前可后可两头)
- 调用方拿到的类型跟传进去的不一样,但用起来没感觉
5.2 该用的信号
当脑子里有这种想法的时候:
- "我不想改这个类的源码,但想给它加点功能" → 装饰器零侵入
- "这个功能不是所有人都需要" → 按需挂载
- "可能要叠加,而且顺序可能有影响" → 一层层套
- "我想运行时动态决定加不加" → 运行时组合,不是编译时继承
5.3 警惕点
| 警惕点 | 说明 |
|---|---|
| 调用栈变深 | 套了 5 层 Wrapper,debug 时栈会很长,心里要有数 |
| 半透明蔓延 | 特有方法越来越多、到处强转时,停下来重新设计,别硬塞 |
| 内存泄漏 | Wrapper 被缓存/异步持有 → 原始对象连带整个请求上下文释放不了 |
| equals / hashCode 陷阱 | Wrapper 没重写,当 Map key 时行为不符合预期 |
5.4 结尾
装饰模式是"不改源码就能加功能"的最优解,但不是免费的------每一层装饰都是一层间接、一层栈、一层心智负担。用它的判断标准很简单:这个增强是"可选的、可叠加的、调用方无感知的"吗?如果是,套上;如果不是,别硬套。