设计模式 05:重写 add 为什么拦不住批量添加
一个合成购物清单需要记录成功加入的条目数。旧版 Before 继承可扩展的 BaseCart,重写 add 后计数:调用 add("paper") 得到 1;再调用 addAll(List.of("pen", "book")),清单已有 3 项,计数却仍是 1。继承允许重写一个入口,却没有保证其他入口都会走它。这与 04 篇 的两阶段方法选择相关,但这里的失败点不是编译器选错重载,而是实际批量调用链根本没有调用被重写的方法。
沿真实代码找缺口
实验里 BaseCart.add 把一个条目加入内部 ArrayList;BaseCart.addAll 则直接调用内部 items.addAll(incoming),没有经过 this.add。子类只重写了 add:
1 | |
因此单条路径调用 Before.add → BaseCart.add → ArrayList.add,批量路径调用 BaseCart.addAll → ArrayList.addAll。两条路径都让清单变长,但只有前一条递增 Before.insertionCount。这里的箭头来自 labs/05/src/main/java/blog/examples/lab05/{BaseCart,Before}.java 中的实际调用,以及测试对计数 1 与长度 3 的断言,并非对 Java 库内部源码调用路径的推测。
flowchart LR
A[Before.add] --> B[BaseCart.add] --> C[内部 List.add]
D[Before.addAll] --> E[BaseCart.addAll] --> F[内部 List.addAll]
G[Before.insertionCount] -.只有单条路径更新.-> A
Java SE 21 JLS §8.4.8.1给出实例方法重写的语言规则;该规则不承诺另一个名字的批量方法必定回调单条方法。ArrayList.addAll Javadoc说明将指定集合元素依次追加到末尾,并不把“必须逐个调用可被子类重写的 add”列为公开契约。本例只按自己的 BaseCart 源码断定绕过,不猜测某个 JDK 构建的内部实现。
两种修法,先问需不需要计数器
After 不继承 BaseCart,而是持有它;对外的 addAll 遍历输入,每项都委托本类的 add,成功加入后才增加计数。这样单条、批量都经过同一条受控路径。新增一条清单路径时,还必须检查是否经过这个入口;组合不会凭空消除维护成本。
1 | |
Alternative 更直接:若清单没有删除操作,而需求只问“当前有多少项”,直接返回 cart.items().size() 即可,不储存第二份可能失准的状态。这个例子里两种修法都断言加入 paper 后为 1、再批量加入 pen 与 book 后为 3;三种实现的原行为均断言元素及顺序是 [paper, pen, book],旧版新需求的违约也由明确断言复现。
| 设计 | 两条加入路径后 | 额外状态与边界 |
|---|---|---|
继承后仅重写 add |
3 项,计数 1 | 批量路径绕过重写 |
组合后统一经本类 add |
3 项,计数 3 | 自己负责覆盖新增入口;逐项添加并非批量原子操作 |
不存计数器,查询 size |
3 项,计数 3 | 仅适合“当前项数”;若加入过后可以删除,累计插入次数不等于当前长度 |
本篇只接收非空的示例字符串与 List.of 批量输入,没有删除、并发或持久化;遇到批量输入中途抛异常时,After 可能已添加前几项,不能从本次绿色测试推断失败原子性。若目标是累计写入次数或审计记录,就要先规定删除、重复条目、失败回滚的行为,不能直接用 size 替代。order-core 中的订单预留没有此类清单需求,所以这里仅是独立 labs/05 实验,不把抽象强加给主线。
验证与练习
2026-10-02 在 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 下运行 ./mvnw -B -ntp verify,修正实验 POM 后退出码 0;05 的 6 次测试及 00–04 的 34 次回归通过。首次执行因本实验 POM 关闭标签拼错退出 1,原始输出和修正后输出均见 examples/design-patterns/evidence/05/,不能把失败运行说成通过。
- 增加
remove(String):需求改为累计成功加入次数,删除后仍应保持原计数。先给Alternative写“加入 3、删除 1、累计仍为 3”的失败断言,再决定是否保留独立计数、是否允许重复条目;运行旧合同回归。不要在该场景继续拿size充当累计次数。 - 若
addAll的第二个元素为null,After遍历时可能已加入第一项。为“拒绝整个批量输入且清单不变”写失败断言,比较先完整验证再加入与暂存副本两种实现对内存和状态的要求;未实测前不要声称批量操作具有事务性。
参考与系列导航
- Java SE 21 JLS §8.4.8.1:实例方法重写的语言条件。
- Java SE 21 ArrayList.addAll:公开的批量追加行为,并无子类回调保证。
- 04 多态与动态分派 ← 05 继承、组合与委托(本篇) → 06 高内聚低耦合。






