设计模式 E07:真实库里能看到哪些模式
看到 ArrayList.Itr、BufferedInputStream、PropertyChangeSupport 这类 JDK 类,很容易把它们直接贴上 Iterator、Decorator、Observer 的标签。标签本身并不是证据。真实库里的类经常同时承担兼容性、性能、序列化、线程安全和历史 API 约束;只看类名、继承关系或“持有另一个对象”,会把结构相似误读成模式意图。
本章固定到 OpenJDK jdk-21+35 对应提交 890adb6410dab4606a4f26a942aed02fb2f55387。本机运行环境是 Homebrew OpenJDK 21.0.10;它只用于验证 Java 21 标准库行为和 src.zip 路径存在,不声明本机发行包源码与上游 GA 提交逐字一致。
Iterator 要看调用者拿到什么遍历协议
OpenJDK 的 ArrayList.iterator() 在固定提交里返回内部 Itr;Itr 持有 cursor、lastRet 和 expectedModCount,next() 先检查结构性修改,再按当前位置返回元素。源码路径是 src/java.base/share/classes/java/util/ArrayList.java,入口在 iterator(),实现类在 Itr。
这和本系列第 34 篇的教学 Iterator 对得上:调用方依赖 Iterator 协议,而不是依赖内部数组和下标细节。labs/E07 先用 After.readWithIterator 遍历 paper, pen,再在拿到迭代器后修改原 ArrayList,断言后续 next() 抛 ConcurrentModificationException。这个断言只说明 fail-fast 边界存在;它不证明并发安全,也不把所有“能遍历”的 API 都叫 GoF Iterator。
1 | |
真实库中的模式识别先找调用者稳定依赖的协议。Iterator 的证据不是“类里有 cursor”,而是调用方能通过统一遍历协议访问不同存储,同时边界条件有明确行为。
Decorator 要看包装后增加了什么责任
FilterInputStream 的 Javadoc 和源码说明它保存一个被过滤的 InputStream,默认把请求转发给这个被包装对象;BufferedInputStream 继承它,并增加缓冲、mark 和 reset 支持。固定提交里的源码路径分别是 src/java.base/share/classes/java/io/FilterInputStream.java 与 src/java.base/share/classes/java/io/BufferedInputStream.java。
labs/E07 用 BufferedInputStream(new ByteArrayInputStream(...)) 验证 markSupported()、mark()、reset() 和继续读取后的结果。这里把它称作 Decorator 是有边界的:它保留 InputStream 接口,包装另一个 InputStream,并添加可观察的新责任。反例是 PassthroughInputStream extends FilterInputStream。它也有同样的继承形状,但只是转发;旧的 Before.byShapeOnly 会把它标成 Decorator,测试则用一个不支持 mark 的输入流证明它没有新增缓冲行为。
1 | |
结构相似只给出候选,不给出结论。Decorator 的关键不是“继承同一个父类”,而是在相同接口下包住原对象,并把附加职责放在包装层。
PropertyChangeSupport 更适合说成监听支持
PropertyChangeSupport 位于 src/java.desktop/share/classes/java/beans/PropertyChangeSupport.java。它维护 PropertyChangeListener 列表,提供 addPropertyChangeListener、removePropertyChangeListener 和 firePropertyChange;当旧值和新值都非空且相等时,不派发事件。Javadoc 还声明这个支持类是 thread-safe。
它和 Observer 的关系应谨慎表达。它确实让发布者不需要知道每个监听者的具体动作,也允许多个监听者响应同一个属性变化;但它的 API 名称、JavaBeans 历史、属性名过滤和序列化约束,都比 GoF 书里的 Subject/Observer 结构更具体。本文把它称作“监听者/Observer 风格的发布订阅支持”,不把它硬说成 GoF Observer 的直接实现。
labs/E07 只断言三个本地行为:draft -> paid 会送达,paid -> paid 不送达,退订后 paid -> closed 不送达。没有测试 Swing 事件线程、跨进程序列化、监听器异常策略或并发注册。
1 | |
真实库的命名、历史和 API 契约要优先于模式标签。能说“这个类承担发布订阅协作的一部分”,不等于可以忽略它自己的领域语义。
三个例子的调用证据
flowchart LR
A[After.readWithIterator] --> B[ArrayList.iterator]
B --> C[ArrayList.Itr.next]
C --> D[fail-fast 检查 expectedModCount]
E[After.readThroughFilter] --> F[BufferedInputStream]
F --> G[被包装的 ByteArrayInputStream]
F --> H[mark/reset 缓冲责任]
I[After.propertyEvents] --> J[PropertyChangeSupport]
J --> K[PropertyChangeListener]
J --> L[相等值不派发]
这张图只说明三个调用链各自验证了什么:第一个验证统一遍历协议,第二个验证包装后新增缓冲责任,第三个验证监听注册和派发规则。它不把所有相似 API 都强行归入 GoF,也不把 PropertyChangeSupport 写成完整异步 Observer 框架。
三个判定问题
看真实库时,模式名应排在证据之后。一个可复查的判断至少回答三个问题:
- 调用者稳定依赖的接口或协议是什么。
- 被稳定下来的变化方向是什么。
- 测试能否证明这个协作边界,而不是只证明类名相似。
如果这三个问题回答不上来,用 API 自己的名字更诚实。Alternative.nameApisInsteadOfPatterns() 直接写“ArrayList.iterator 返回 fail-fast Iterator”、“BufferedInputStream 添加缓冲与 mark/reset”、“PropertyChangeSupport 管理监听注册和派发”。这比把所有例子都塞进 GoF 名称更不容易误导读者。
| 案例 | 可说的模式关系 | 本章证据 | 不能外推 |
|---|---|---|---|
ArrayList.Itr |
与 Iterator 意图吻合 | 统一遍历协议、fail-fast 边界 | 并发安全 |
BufferedInputStream |
与 Decorator 意图吻合 | 包装 InputStream 并添加缓冲、mark/reset |
所有 FilterInputStream 子类都是有效装饰器 |
PropertyChangeSupport |
Observer 风格的监听支持 | 注册、派发、相等值不派发、退订 | 完整异步消息或 GoF 原书结构一一对应 |
实验、边界与练习
在 examples/design-patterns/ 执行:
1 | |
4 项测试通过;环境、命令、源码路径检查、OpenJDK tag object 和固定 SHA 源码副本见 examples/design-patterns/evidence/E07/RUN.md 与 examples/design-patterns/evidence/E07/OPENJDK.md。本机 src.zip 只用于路径存在性和运行行为验证。
- 把
PassthroughInputStream改成真正缓存第一个字节的包装器,先保留旧 passthrough 反例,再新增 mark/reset 或重复读取行为断言,判断它是否仍应叫 Decorator。 - 给
PropertyChangeSupport增加一个会抛异常的监听器,先运行现有订阅/退订合同,再查 JDK 行为并写出异常传播断言;不要从“Observer”标签推断错误策略。
上一节:E06 从 DIP 到 Ports and Adapters;下一节:E08 遗留代码的小步重构;可选回到:39 从变化需求保留必要模式。
参考资料
- OpenJDK
jdk-21+35固定提交890adb6410dab4606a4f26a942aed02fb2f55387:ArrayList.java、FilterInputStream.java、BufferedInputStream.java、PropertyChangeSupport.java。 - Oracle Java SE 21 API:
ArrayList、FilterInputStream、BufferedInputStream、PropertyChangeSupport。 - 实验代码与输出:
examples/design-patterns/labs/E07/,examples/design-patterns/evidence/E07/RUN.md。





