设计模式 14:包依赖为什么成环
结账负责合计,运费负责免邮条件。原本结账调用运费包;后来运费为了复用“满 100 元”判断,反过来调用结账包。即使运行时没有无限递归,pricing → shipping → pricing 也使两个包的变更和编译方向缠在一起。下一次物流部门把免邮线调整到 80 元,哪一侧应该决定规则?
先分开运行时调用与包依赖
labs/14 的 Before 在结账端调用 Shipping.fee,运费端调用 Checkout.qualifiesForFreeShipping。它能算对 0、9_999、10_000 分的旧结果,但包层有环;不能仅从单次请求结果推导依赖图健康。最小修复让 Shipping 自己持有运费规则,让 Checkout 只依赖稳定的 ShippingRule,组装点传入具体运费实现。
1 | |
实际包边由 jdeps -verbose:package labs/14/target/classes 报告:旧版 before.pricing → before.shipping 以及反向的 before.shipping → before.pricing;新版 after.pricing → after.core、after.shipping → after.core。这是包级静态依赖,不是 Maven 模块之间的环(本实验所有这些包仍在同一个 lab-14 模块内),也不是运行时吞吐量证据。组装点要知道两边的具体类型,不应误称整个应用完全没有对具体类的依赖。
新增 80 元免邮要求通过注入 subtotal -> subtotal >= 8_000 ? 0 : 500 验证 Checkout 不改;Before 保留 100 元作为旧合同。若项目只有一个结账入口和一个运费规则,Alternative 在单类方法里写条件也能满足旧合同;为这个微小例子单独拆包反而增加寻找代码的成本。
| 选择 | 谁依赖谁 | 改免邮规则要动的边界 |
|---|---|---|
Before |
两个包相互指向 | 运费借用结账中的条件 |
After |
结账、运费分别指向规则端口 | 替换运费实现并在入口组装 |
Alternative |
单类,没有跨包边 | 修改本地条件 |
六条包原则分别约束什么
Robert C. Martin 的 Design Principles and Design Patterns 将包原则分成内聚和耦合两组。它们讨论发布与依赖单元;本例的 Java package 只用于观察静态边,并没有实现独立版本发布。
| 原则 | 含义 | 在订单工程中需要回答的问题 |
|---|---|---|
| REP:发布与复用等价 | 复用单元要能作为版本化发布单元交付 | 外部复用金额类型时,是否必须同时升级报表实现? |
| CCP:共同封闭 | 由同类变化牵动的类宜放在一起 | 调整运费政策是否需要跨多个发布单元修改? |
| CRP:共同复用 | 不共同复用的类不宜被绑定到同一单元 | 只使用库存查询的调用方为何要接收通知实现变化? |
| ADP:无环依赖 | 发布单元的依赖图应避免环 | 报价与运费是否必须互相等待对方版本? |
| SDP:稳定依赖 | 依赖宜指向更难改动的单元 | 许多调用方依赖的规则接口是否反而依赖易变的渠道实现? |
| SAP:稳定抽象 | 难改动的单元宜通过抽象保留扩展能力 | 新增运费策略能否实现端口,而不改所有调用方? |
CCP 倾向把一起变动的实现聚集起来;CRP 则避免外部使用者被不相关的变动牵连;REP 还要求版本与维护边界。若结账与 CSV 报表因同一次财务调整一起修改,但库存客户端从不使用 CSV,把它们放进一个大包可能降低内部协调成本,却扩大外部升级范围。必须区分“谁一起修改”和“谁一起使用”,不能仅按类名相近分组。
这里的稳定性关注改动的牵连程度,不等于近期很少提交。原文用输入、输出耦合讨论它;一个接口被许多模块依赖,修改签名会影响这些调用方,即使它只有几行代码。SAP 也不要求所有底层类都变成接口:金额值、固定算法等具体实现仍可以保留,只有已出现的变化边界需要扩展点。
本例能直接检查的是 ADP 对应的包环,以及运费端口带来的替换方向。REP、CCP、CRP 需要真实复用者、发布安排和变化记录;本次测试没有提供这些证据。jdeps 不会证明六条原则全部满足,也不能从依赖边数量直接得出设计好坏。
flowchart LR
PricingBefore[before.pricing] --> ShippingBefore[before.shipping]
ShippingBefore --> PricingBefore
PricingAfter[after.pricing] --> Core[after.core ShippingRule]
ShippingAfter[after.shipping] --> Core
验证与边界
执行 ./mvnw -B -ntp -pl labs/14 -am test、jdeps -verbose:package labs/14/target/classes 和累计 ./mvnw -B -ntp verify;原始命令、环境和退出码见 examples/design-patterns/evidence/14/RUN.md。测试含旧金额边界、替换免邮线的断言;没有模拟依赖管理器的模块发布,也没有运行并发请求。
练习:
- 新增“周末免邮 60 元”,先为原工作日规则和周末新增规则分别写边界断言,再替换组装点并运行完整回归;绘出新增的静态依赖边。
- 假设运费永远不会独立变化且只有一个调用者,移除端口写成
Alternative,说明删去抽象后仍满足哪些测试、失去哪个替换能力。
参考资料
- Robert C. Martin, Principles and Patterns:https://objectmentor.com/resources/articles/Principles_and_Patterns.pdf ,pp.17–26 覆盖 REP、CCP、CRP、ADP、SDP、SAP。
- 实验代码:
examples/design-patterns/labs/14/。
上一节:13 重复与抽象哪个更贵;下一节:15 阅读模式先看变化。






