3158 字
约 10 分钟
8
S18-J05 · 设计原则与质量属性:从"模式"回到"度量"

适用读者:能把七原则背成口诀、但评审时只能说"这个不太符合开闭"的人;准备写架构方案或做设计评审的人。
前置: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 的"优先级和平衡"):

  1. SRP vs CRP:类拆得越细,组合关系越多,装配代码反而变复杂。→ 以"变化原因"为拆分依据,不以"方法数量"。
  2. ISP vs LoD:接口拆得太小,客户端要依赖一堆小接口,反而更耦合。→ 以"谁在用"为粒度依据。
  3. 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 失败语义:把"失败"设计成契约

这是七原则里最容易被忽略的一条实践:失败要有统一的形状。落到代码上是三件事:

  1. 错误码表(不要用异常消息当契约);
  2. 可重试 vs 不可重试明确标注(决定上游是重试还是告警);
  3. 部分成功要显式(批量导入 100 条,成功 97 条怎么返回)。

这三件事做好,"高可用"才不是只堆机器——因为大部分"不可用"其实是"失败没人知道怎么处理"。


5. 常见误区

误区 真相
"七原则要全部满足" 它们互相冲突,取舍依据是"高内聚低耦合",不是逐条打勾
"开闭原则 = 永远不改旧代码" 是"扩展优先";修 bug、改需求照样要改旧代码
"抽象度越高越好" A 高而 I 低(稳定且全抽象)意味着没人实现它;D≈1 是警告
"质量属性可以说'要高可用'" 不含量化度量的说法无法验收,也无法定位责任
"缓存一致性靠清缓存" 清缓存是运维手段;设计手段是"输入变 → key 变"
"耦合越低越好" 零耦合意味着没有协作;目标是"耦合在正确的地方"

6. 自测

  1. 为什么说开闭原则是"目标"而其余六个是"手段"?举一个两原则冲突的例子。
  2. 一个包的 Ca=0、Ce=8、A=0,D 是多少?这个包健康吗?
  3. 写出你手上某个接口的"可用性场景"六要素,并给出响应度量。
  4. 缓存 key 里漏掉"模型版本"会发生什么?属于哪一类 bug?
  5. "失败要留现场"落到日志字段上,至少要有哪三项?
  6. 为什么度量包级指标之前必须先定限界上下文?

下一篇

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。

S18-J05 · 设计原则与质量属性:从"模式"回到"度量"
https://www.clxhxhhr.top/posts/5439/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。