设计模式 34:遍历协议隐藏了什么
订单展示方要依次读取 paper, pen。旧接口直接把内部 List 交出来,展示方可以在读取过程中添加元素,也不得不知道订单实际以列表保存。若以后用双端队列收集商品,调用方能否仍使用同一个遍历协议,且得到明确的空列表、末尾与修改语义?
让调用方依赖遍历而非存储
labs/34 的 Before.items 与调用者传入的 ArrayList 是同一个引用;测试追加 pen 后旧对象也多了一项。After 的 ListOrder 构造时复制输入,DequeOrder 从相同数据构造队列;两者都实现 Iterable<String>,调用者只需 for (String item : order)。示例约定新订单在构造时形成稳定快照,后续修改原列表不会改变结果。DequeOrder.iterator() 再提供不可修改的遍历快照;对空迭代器调用 next() 抛 NoSuchElementException,在迭代器上 remove() 抛 UnsupportedOperationException。
1 | |
上面的 display 只是调用方动作的示意;可运行断言位于实验模块。Iterator 把如何逐步访问元素与内部容器分开,但不会自动规定并发修改策略。本章刻意选构造快照而不是把 Java 某个容器的 fail-fast 行为当安全保证。Alternative 直接返回一个新列表:如果只有一种容器且所有调用方只想拿到值清单,这样更简短,不过仍会把“列表”当作对外 API。
| 方案 | 调用者修改原列表 | 末尾与删除 |
|---|---|---|
Before |
订单内容跟着变 | 暴露原列表,责任在外部 |
After |
两种容器结果不变 | 末尾抛异常,remove 不支持 |
Alternative |
返回一个新的列表副本 | 无专用遍历协议 |
这个教学协议不处理大数据分页、流式 IO 或并发访问;只测单线程快照与遍历边界。
flowchart LR
Caller[for 遍历调用方] --> Protocol[After / Iterable]
List[ListOrder] -.实现.-> Protocol
Deque[DequeOrder] -.实现.-> Protocol
Protocol --> Iter[Iterator]
Iter --> Snapshot[构造时数据快照]
验证与练习
在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/34 -am test 和累计 ./mvnw -B -ntp verify;真实版本、原始输出、断言边界见 examples/design-patterns/evidence/34/RUN.md。
- 新增“允许删除遍历过程的当前元素”的需求,先让两种容器的旧顺序与快照合同通过,再明确删除影响原数据还是仅影响游标,写失败断言后实现。
- 如果永远是小的只读列表,删去两种容器包装只返回
List.copyOf,运行顺序和空列表断言;说明这种简化失去什么扩展边界。
参考资料
- GoF 原书公开图书馆 PDF,5.4 Iterator,目录页标注起始页 289:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Java SE 21
ArrayListJavadoc:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayList.html 。 - OpenJDK
ArrayList.Itr固定源码证据:examples/design-patterns/evidence/E07/OPENJDK.md。 - 实验代码:
examples/design-patterns/labs/34/。
上一节:33 对象状态如何约束操作;下一节:35 协作集中会形成大对象吗。






