适用读者:能把七原则背成口诀、但评审时只能说"这个不太符合开闭"的人;准备写架构方案或做设计评审的人。
前置:S18-J01–J04(模式与判据)。术语沿用 S11(数据一致性)、S17(分布式质量)。
覆盖旧文:A-951(108 点)、A-548(39 点)、A-552(75 点),以及 A-2609 的"六大原则"章节(27 点,duplicate_ref指向本篇)——合计 249 点。
扩展点:E-S18-008(结构度量)、E-S18-009(质量属性场景)。
上一篇:S18-J04 · 行为型模式:谁通知谁、按什么顺序
下一篇:S18-J06 · DDD 战术建模:实体、值对象、聚合。
1. 一句话结论
七个原则不是七条法律,它们只是同一个目标(高内聚、低耦合)在六个维度上的投影;能被度量的是耦合与内聚,不能被度量的是"你有多遵守原则"。
A-951 的原话给了正确口径:开闭原则是总目标,其余六个是手段,而且它们会互相打架——"别死板地每条都严格遵守,根据实际场景做取舍"。本文的任务是把这句话变成可操作的评审语言。
| 原则 | 约束的对象 | 违反时的现场信号 | 可用的度量代理 |
|---|---|---|---|
| 开闭 OCP | 变化的方向 | 每加一个类型就改同一个 switch |
改动扩散面(一次需求改几个文件) |
| 单一职责 SRP | 类的变化原因 | 类名含 And/Manager/Util;被 3 个以上不相关模块调用 |
LCOM(方法-字段共现度) |
| 里氏替换 LSP | 继承的行为契约 | 子类抛父类没有的异常、重写后改变前置条件 | 契约测试覆盖率 |
| 依赖倒置 DIP | 依赖的方向 | domain 里 import 了 infra/jdbc |
包依赖图(见 §2、A02 的 ArchUnit) |
| 接口隔离 ISP | 接口的粒度 | 实现类被迫写空方法 | 接口方法数 / 实现类空实现数 |
| 迪米特 LoD | 对象交互的深度 | a.getB().getC().doX() 长链 |
方法调用链深度 |
| 合成复用 CRP | 复用的方式 | 为复用 3 个方法而继承一个 800 行的基类 | 继承深度 vs 组合字段数 |
原则冲突的三个真实例子(都出自 A-951 的"优先级和平衡"):
- SRP vs CRP:类拆得越细,组合关系越多,装配代码反而变复杂。→ 以"变化原因"为拆分依据,不以"方法数量"。
- ISP vs LoD:接口拆得太小,客户端要依赖一堆小接口,反而更耦合。→ 以"谁在用"为粒度依据。
- LSP vs 现实需求:子类确实需要收紧行为时,说明继承关系本身错了,应改为组合。
2. 把"耦合"变成数字:Martin 的四个度量
Robert C. Martin 在《敏捷软件开发:原则、模式与实践》里给了一套可算的包级度量(包耦合度量 / 稳定性度量,业内常称 Martin Metrics):
| 度量 | 定义 | 读法 |
|---|---|---|
| Ca 扇入(Afferent) | 有多少个外部包依赖本包 | 越大 → 越多人靠你,越不该乱改 |
| Ce 扇出(Efferent) | 本包依赖多少个外部包 | 越大 → 你越容易受别人影响 |
| I 不稳定性 | I = Ce / (Ca + Ce),范围 0–1 |
0 = 最稳定,1 = 最不稳定 |
| A 抽象度 | A = 抽象类与接口数 / 类总数 |
0 = 全具体,1 = 全抽象 |
| D 距离主序列 | `D = | A + I − 1 |
主序列的含义:稳定的包应该抽象(给别人扩展),不稳定的包应该具体(自己随便改)。落在这条线外就是设计问题。
算一遍真实例子。用本系列 A02 实验工程(4 个包 6 个类,源码 import 手工统计,非工具输出):
| 包 | Ca | Ce | I | A | D | 判读 |
|---|---|---|---|---|---|---|
domain |
2(app、infra) | 0 | 0.00 | 1/3≈0.33 | 0.67 | 最稳定但抽象不足:Order 是具体类,别人只能依赖它而不能替换它 |
app |
1(trigger) | 1(domain) | 0.50 | 0 | 0.50 | 稳中有变,可接受 |
infra |
0 | 1(domain) | 1.00 | 0 | 0.00 | 不稳定 + 具体 = 正好在主序列上(实现类就该这样) |
trigger |
0 | 1(app) | 1.00 | 0 | 0.00 | 同上 |
这张表最有价值的一行是 domain:D=0.67 说明领域包"很稳定但不抽象"。这正是 DDD 里"领域模型应该通过接口对外(仓储/端口),把具体实现放基础设施层"的度量解释——不是教条,是 D 值在提醒你。
CBO(对象间耦合) 是类级的另一把尺子:一个类引用了多少个别的类。经验阈值:业务类 CBO > 10 就要看是不是职责过多;实体/值对象 CBO 应该很小(它们只依赖自身类型)。
注意度量的边界(避免变成新的八股):
- 度量是诊断工具,不是评分表。为了把 I 调到 0.5 去硬拆包,是本末倒置。
- 这些数字依赖包划分;包划错,数字全错。所以先定边界(A01),再谈度量。
- 工具能算(jdeps、ArchUnit、Sonar),但手工算一遍 4 个包的收益大于跑 10 个报表——你会真正理解公式在说什么。
3. 质量属性场景:把"要高可用"写成六要素
"高可用""高性能""可扩展"这些词在评审会上无法验证。软件架构质量属性(SEI 的 ATAM 方法)要求把它写成六要素场景:
| 要素 | 要回答的问题 |
|---|---|
| 刺激源 | 谁/什么触发了它?(用户、运维、外部系统、时钟) |
| 刺激 | 触发的是什么事件?(请求洪峰、节点宕机、发布、配置变更) |
| 制品 | 作用在哪个部分?(下单接口、订单库、网关) |
| 环境 | 什么条件下?(正常态、降级态、机房切换中) |
| 响应 | 系统做了什么?(排队、熔断、切主、返回兜底值) |
| 响应度量 | 怎么量化算通过?(P99 < 300ms、错误率 < 0.1%、RTO < 5min) |
三个写好的例子(可直接改成你项目的版本):
| 属性 | 场景(压缩写法) | 响应度量 |
|---|---|---|
| 可用性 | 单可用区故障时,下单接口在 60s 内切到另一区 | 错误率 < 1%,RTO ≤ 60s,RPO = 0(同步复制) |
| 性能 | 大促 0 点,10 倍日常流量持续 10 分钟 | P99 ≤ 300ms,超时率 < 0.5%,队列积压 < 5 分钟可消化 |
| 可修改性 | 新增一种支付渠道,不改下单主流程 | 改动 ≤ 2 个文件、≤ 1 人日、回归用例 ≤ 30 条 |
判据:写不出"响应度量"的场景,就不是场景,是口号。而度量必须能被现有监控测出来——测不出来的指标等于没写。
4. 三个真实取舍
旧文 A-548、A-552 是两份"决策笔记",它们的价值不在结论,在方案谱系 + 判据。这三条可以直接搬进 ADR(见 A04 §6)。
4.1 缓存失效:"改了就更新"怎么接全(A-548)
| 方案 | 做法 | 代价 | 适用 |
|---|---|---|---|
| ① 手动失效 | 按钮 + 重启 | 靠人、易漏 | 只作紧急兜底 |
| ② 内容指纹 | 把输入算 hash 进 key | 一行 hash | 默认首选 |
| ③ 依赖追踪 | 记录结果依赖哪些输入 | 中 | 依赖复杂、多共享 |
| ④ 乐观失效 + 后台重建 | 改动即作废,后台重算 | 中高 | 高频交互编辑器 |
| ⑤ 请求级去重 | 相同输入幂等复用 | 中高 | 想省外部调用费 |
判据:cacheKey = f(源数据, 所有可改变结果的参数, 版本类信息)。第三项最容易被漏——不把"模型/prompt 版本"放进 key,换模型后仍会命中旧结果。
反模式:用"重启服务/手动刷新"兜底。它不解决问题,只是把问题变成"用户偶尔看到旧数据,而且不报错"。A-548 的原话是:缓存失效是一等需求,不是事后补丁。
4.2 应对"变化的外部依赖"(A-552)
| 思想 | 一句话 | 落地检查 |
|---|---|---|
| 脆弱点隔离 | 把"会变的"关进一个小房间 | 选择器/协议适配是否集中在一个文件 |
| 备选回退 | 给同一目标准备一串有序退路 | 是否有按优先级排列的备用方案 |
| 失败留现场 | 报错要带"试过谁 + 现场" | 失败日志是否含尝试清单与快照 |
| 优先稳定锚点 | 能钉 data-e2e 这类标记,别靠长相 |
定位是否优先用稳定标识 |
A-552 自己给了两条边界,非常值得抄进评审:① 补丁是捷径,累积成债(退路多到难维护时,该换入口而不是继续叠);② 隔离要"被维护",不是"藏起来"(扔进没人管的文件不算隔离)。
4.3 失败语义:把"失败"设计成契约
这是七原则里最容易被忽略的一条实践:失败要有统一的形状。落到代码上是三件事:
- 错误码表(不要用异常消息当契约);
- 可重试 vs 不可重试明确标注(决定上游是重试还是告警);
- 部分成功要显式(批量导入 100 条,成功 97 条怎么返回)。
这三件事做好,"高可用"才不是只堆机器——因为大部分"不可用"其实是"失败没人知道怎么处理"。
5. 常见误区
| 误区 | 真相 |
|---|---|
| "七原则要全部满足" | 它们互相冲突,取舍依据是"高内聚低耦合",不是逐条打勾 |
| "开闭原则 = 永远不改旧代码" | 是"扩展优先";修 bug、改需求照样要改旧代码 |
| "抽象度越高越好" | A 高而 I 低(稳定且全抽象)意味着没人实现它;D≈1 是警告 |
| "质量属性可以说'要高可用'" | 不含量化度量的说法无法验收,也无法定位责任 |
| "缓存一致性靠清缓存" | 清缓存是运维手段;设计手段是"输入变 → key 变" |
| "耦合越低越好" | 零耦合意味着没有协作;目标是"耦合在正确的地方" |
6. 自测
- 为什么说开闭原则是"目标"而其余六个是"手段"?举一个两原则冲突的例子。
- 一个包的 Ca=0、Ce=8、A=0,D 是多少?这个包健康吗?
- 写出你手上某个接口的"可用性场景"六要素,并给出响应度量。
- 缓存 key 里漏掉"模型版本"会发生什么?属于哪一类 bug?
- "失败要留现场"落到日志字段上,至少要有哪三项?
- 为什么度量包级指标之前必须先定限界上下文?
下一篇
S18-J06 · DDD 战术建模:实体、值对象、聚合
原则与度量讲的是"结构好不好",下一篇进入领域建模的实体层:什么该是实体、什么该是值对象、聚合边界怎么划、仓储为什么必须由领域层定义接口。这是 S18 从"通用设计"转向"领域设计"的拐点。
本篇属于系列《S18 · 架构方法与领域建模》。旧文来源:A-951/A-548/A-552(A-2609 原则章节为 duplicate_ref);扩展点 E-S18-008、E-S18-009。度量为按源码 import 手工统计(4 包 6 类),公式出自 Martin《敏捷软件开发:原则、模式与实践》的包度量一节;质量属性六要素口径出自 SEI ATAM。