设计模式 07:谁在要求修改报价与报表
第 06 篇把计算、内存保存和展示的传播范围拆开了。本篇不再比较同一个报价文本前缀,而是让定价规则和导出报表产生新的独立变化:合成的定价方要求会员使用优惠券时,在九折后再减 1.00;报表使用方要求 SKU 含逗号或双引号时,导出行正确加引号并转义。两种要求都可能出现在同一份报价单上,却不由同一方定义。
Before.quoteSheet 有两个动作:计算总额,随后把 SKU 与金额拼成一行 CSV。普通客户买两件单价 10.00 的 paper,得到 20.00 与 paper,20.00;会员得 18.00 与 paper,18.00。这是三种实现都运行的同一原行为合同。新增请求里,旧版接受 coupon=true 仍返回会员 18.00,接受 strictCsv=true 仍把 SKU paper,lined 输出成 paper,lined,20.00:第三列看起来像金额,实际 SKU 已被拆开。旧版的错误输出都在测试中明确断言,而不是让验证入口一直失败。
定义职责要先找到提出变化的人
Robert C. Martin 在 The Single Responsibility Principle 中,从“一个修改原因”进一步问软件要对谁负责,用薪酬计算、工时报告和存储分别由不同角色指定规则的例子说明边界。这里的“定价方”“报表使用方”只是教学场景中的两种需求来源,不是虚构的生产组织;SRP 不等于“一类只能有一个方法”。
按这两种来源,After 的入口仍返回同一个 QuoteSheet,但实际调用链是:
1 | |
PriceRule.total 只处理会员优惠券;CsvReport.row 只处理导出字段。定价规则变化时 CSV 不必知道券的算法,导出约定变化时价格不必知道 SKU 里的逗号。Alternative 仍用一个 quoteSheet 方法里的两段条件分支实现同样结果;在需求来源稳定、协作数量少的情况下,它比增加两个协作者更直接,不能仅凭类数判定优劣。
flowchart LR
PriceActor[定价需求方] --> Price[PriceRule.total]
ReportActor[报表使用方] --> Csv[CsvReport.row]
Entry[After.quoteSheet] --> Price
Entry --> Csv
Price --> Amount[计算金额]
Csv --> Row[导出行]
| 需求卡 | 旧版观测 | 拆分后与简单替代的可失败断言 | 不应被改变的另一方合同 |
|---|---|---|---|
A:会员券减 1.00 |
仍为 18.00 |
会员 17.00、普通客户仍 20.00 |
普通 SKU 仍按原格式输出 paper,17.00 |
| B:SKU 含逗号或双引号 | paper,lined,20.00 |
"paper,lined",18.00;paper"lined 中引号写成两个 |
会员仍为 18.00 |
| A+B | 两个开关都忽略 | 会员 17.00、导出 "paper,lined",17.00 |
金额只由定价规则决定 |
单独做 A 或 B,旧版都是修改 Before.quoteSheet,拆分后分别只修改 PriceRule.total 或 CsvReport.row;实现文件数并不会自动变少。分离的证据是独立的变化来源与受保护的另一方合同,而不是方法个数。After 的两个协作者既不是预装的插件系统,也不需要容器注入。
这里的 CSV 只在本实验输入上核对逗号和双引号的转义,没有测试字符编码、CRLF、表头、批量多行和与外部 CSV 解析器的互操作,不能声称完整的 CSV 标准兼容。OrderLine 单价 10.00 让会员折后减 1.00 仍为非负;未测试极小订单或多券叠加,不能据此把案例规则作为生产金额算法。
验证和练习
2026-10-02 在 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 下运行 ./mvnw -B -ntp verify,退出码 0;07 的 10 次测试及 00–06 的 50 次回归均通过。输入、命令与完整原始输出见 examples/design-patterns/evidence/07/;实验没有更改累计 order-core,也没有真实报表平台。
- 报表使用方要求导出行添加
orderId列,但定价方要求标准和会员金额保持不变。分别给三种实现写“新增列、会员金额仍18.00、优惠券金额仍17.00”的断言,并检查After的修改位置;如果PriceRule必须接收报表列名,就回头检查边界是否泄漏。 - 金额极小时会员券可能把价格减为负数;先写具体输入和拒绝/钳零两种互斥契约中的一种,再判断这是谁提出的业务规则。不要因为
PriceRule只有一个方法就省略校验,也不要把未讨论的政策伪装成 Java 规范。
参考与系列导航
- Robert C. Martin:The Single Responsibility Principle:职责与变化提出者的第一手说明。
- 06 高内聚低耦合 ← 07 SRP 单一职责(本篇) → 08 OCP 开闭原则。






