一份报表原本只生成 csv:A,现在审阅方要 json:A。加载数据失败时绝不能继续格式化,更不能留下“已写入”的轨迹。用一个可继承的流程固定“读取 → 格式化 → 记录写入”,比单纯为两种前缀各复制一份流程更合理吗?

骨架固定,局部步骤交给子类

Before.report("A") 直接返回旧 CSV 文本。After.run() 是 final,依次记录 read、调用 read(),记录 format、调用 format(raw),最后记录 write 并返回文本;Csv、Json 子类分别提供读值和格式化行为。这里的 write 仅是内存轨迹标记,并没有落盘。测试断言 CSV 旧文本保持,新增 JSON 文本是 json:A,完整轨迹恰为三步。

1
2
3
4
5
6
trace.add("read");
String raw = read();
trace.add("format");
String result = format(raw);
trace.add("write");
return result;

当读取抛出异常时,轨迹只含 read,格式化钩子没有调用、也不存在 write 标记。这个短路边界比只检查正常输出更能说明骨架负责什么。Alternative 直接接收格式化函数;若读取来自同一个值、也不需要固定若干步骤,组合方案能少掉两个子类。Template Method 使用继承在局部步骤打开变化点,和 28 的 Strategy 通过组合替换整个算法不同;若子类可以跳过关键校验,则模板合同也会被破坏。本章 run 为 final,但 read、format 的输入输出仍须靠测试约束。

方案 新增 JSON 读取失败时
Before 只有直接 CSV 方法 无读取阶段
After 新子类实现局部钩子 不执行格式化与写入标记
Alternative 新格式函数 需要调用方负责读取与失败处理

没有文件格式解析器、IO 流关闭、外部数据库或真实写入;不能把轨迹标记称为报表已持久化。

flowchart LR
    Caller[报表调用方] --> Run[After.run 固定顺序]
    Run --> Read[read 子类钩子]
    Read --> Format[format 子类钩子]
    Format --> Trace[记录 write 轨迹并返回]
    Csv[Csv / Json] -.实现钩子.-> Read
    Csv -.实现钩子.-> Format

验证与练习

运行 ./mvnw -B -ntp -pl labs/29 -am test 和累计 ./mvnw -B -ntp verify,完整输入、原始输出与环境见 examples/design-patterns/evidence/29/RUN.md。正例是 CSV/JSON;反例是读取错误后误写;边界为异常只留下 read。

  1. 新增“格式化失败”变体,先断言轨迹为 read,format 且没有 write,复跑 CSV/JSON 旧合同,再比较继承钩子与格式函数需要修改哪里。
  2. 假设只有 CSV 和一个固定输入,移除模板写成普通函数,保留旧失败边界的测试,解释何时继承骨架反而过度。

参考资料

上一节:28 三种报价如何共享契约;下一节:30 请求对象能撤销什么。