订单展示方要依次读取 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
2
3
for (String item : order) {
display(item);
}

上面的 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。

  1. 新增“允许删除遍历过程的当前元素”的需求,先让两种容器的旧顺序与快照合同通过,再明确删除影响原数据还是仅影响游标,写失败断言后实现。
  2. 如果永远是小的只读列表,删去两种容器包装只返回 List.copyOf,运行顺序和空列表断言;说明这种简化失去什么扩展边界。

参考资料

上一节:33 对象状态如何约束操作;下一节:35 协作集中会形成大对象吗。