设计模式 06:两次独立改需求,为什么都要碰同一服务
一个报价服务算金额、把金额放进内存 Map,再组装给调用方的文本。最初普通客户买 2 件、单价 10.00,得到 20.00 和 CNY 20.00;会员沿用九折得到 18.00。现在分别收到两张变化卡:A 仅在活动开启时把会员改为八折,B 只把展示文本换成 合计(CNY):金额。两张卡来自不同变化来源,金额保存行为不该因为文案变化而改变。旧版 Before 的计算、内存保存和展示全部挤在同一类里。
变化传播不是类数竞赛
Before.quote 的实际顺序是 line.undiscountedTotal() → 按会员九折计算 → saved.put(id, total) → 拼接 CNY 前缀。它接受 promotion 和 detailed 两个开关,却没有处理新要求;对会员开启活动仍保存 18.00,指定详细展示仍返回 CNY 20.00。测试显式断言这两个旧输出,作为可执行的变化压力,不把基线伪装为已满足需求。
拆开的 After 仍只有一个入口,负责按顺序协作:
1 | |
Pricing 只管金额和活动开关;MemoryQuoteStore 保留本地内存数值;QuoteFormatter 只管字符串。没有预先制造三个接口、注入容器或真实数据库,内存保存也不能称为持久化。前几篇 03 的失败后不变 和 04 的分派规则在这里仍是约束:本篇没有放宽订单预留的契约,也不把增加类数误称为运行时多态。
flowchart LR
A[Before.quote] --> B[计算九折]
A --> C[Map 保存]
A --> D[文本展示]
E[After.quote] --> F[Pricing.total]
E --> G[MemoryQuoteStore.save]
E --> H[QuoteFormatter.format]
分别观察两张卡的传播范围:
| 独立需求卡 | 旧版必须改哪里 | 拆分后必须改哪里 | 不应变化的观测 |
|---|---|---|---|
| A:会员活动价改八折 | Before.quote 的计算段 |
Pricing.total 的计算条件 |
普通客户仍 20.00;会员显示仍用旧格式;保存与计算结果相等 |
B:详细文案改为 合计(CNY):… |
同一个 Before.quote 的展示段 |
QuoteFormatter.format 的展示条件 |
原金额 18.00/20.00 不变;保存值等于原金额 |
这不是“少改了几个文件”:单独做 A 或 B,旧版与拆分版各需修改一个实现文件;同时做两张卡,拆分版反而涉及两个职责文件。收益在于计算与展示各有独立的修改目标和断言,MemoryQuoteStore 完全不参与这两次变化。实验对 After、单类 Alternative 分别运行 A、B 和 A+B:开启会员活动总额 16.00;只改展示仍是会员 18.00;两卡并用是 合计(CNY):16.00,保存值始终与计算结果相等。原行为中普通与会员的价、展示及保存也被三种方案的同一合同覆盖。
Alternative 没有拆出三个职责类,在一个方法里分支并保持同一组合同。若总共只有这两个小改动、调用方也少,它的维护成本可能低于四类协作;但以后若计算、展示与数据来源频繁由不同团队独立变化,就会再次产生共享修改点。先拿实际变化频率和冲突代价说话,不能因为技术层名词整齐就断定必需拆分。
Martin Fowler 的 Presentation Domain Data Layering解释呈现、领域逻辑与数据访问的逻辑边界,并明确指出技术层划分不宜直接当大型系统的顶层模块边界。这里的 Pricing、Store、Formatter 只服务于一个小型报价入口,不是在宣称“三层架构”是普遍答案。九折、八折和展示文案都是合成输入,货币仍沿用示例里的 CNY,没有跨币种兑换需求。
验证范围与练习
2026-10-02 在 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 下运行 ./mvnw -B -ntp verify,退出码 0:06 的 10 次测试及 00–05 的 40 次回归通过。命令、输入、结果和原始输出见 examples/design-patterns/evidence/06/。内存 Map 不模拟事务、并发可见性或真实数据库;这组绿色测试只支持上述顺序调用的声明。
- 第三张卡要求保存时附带审计编号,但报价数字和显示文本必须不变。先对
Before、After、Alternative分别写“报价与展示不变、保存可查询编号”的可失败断言,再比较这次变化触及哪个实现文件,以及After的store是否仍能只负责存取;不要凭预想新增 DAO 接口。 - 把两张卡交给不同开发者分别修改,从当前三个实现的实际方法出发找可能冲突的修改点;若还要求按不同币种显示符号却不改变保存金额,应由哪个职责处理?先写同一金额的两种展示断言,再说明为何当前
CNY固定格式还不足以覆盖币种需求。
参考与系列导航
- Martin Fowler:Presentation Domain Data Layering:逻辑边界及避免顶层技术层划分的范围提示。
- 05 继承、组合与委托 ← 06 高内聚低耦合(本篇) → 07 单一职责。






