设计模式 29:哪些报告步骤必须固定
一份报表原本只生成 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 | |
当读取抛出异常时,轨迹只含 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。
- 新增“格式化失败”变体,先断言轨迹为
read,format且没有write,复跑 CSV/JSON 旧合同,再比较继承钩子与格式函数需要修改哪里。 - 假设只有 CSV 和一个固定输入,移除模板写成普通函数,保留旧失败边界的测试,解释何时继承骨架反而过度。
参考资料
- GoF 原书公开图书馆 PDF,5.10 Template Method,目录页标注起始页 360:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Template Method 模式简定义,InfoWorld/JavaWorld:https://www.infoworld.com/article/2170730/design-patterns-the-big-picture-part-1-design-pattern-history-and-classification.html 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/29/。
上一节:28 三种报价如何共享契约;下一节:30 请求对象能撤销什么。






