27.4 大泥球的形成路径

一句话

大泥球不是 “一次设计失败”,而是五个可识别阶段的连续滑坡:共享数据库 → 跨模块事务 → 循环依赖 → “公共模块” 黑洞 → 无人敢改;每一阶段都有可观测信号,抓早一步,成本差一个数量级。

五个阶段

阶段 症状 可观测信号 阻断动作
① 共享数据库 所有模块读写同一批表 任意 schema 变更都要全量回归 每模块独立 schema
② 跨模块事务 一次请求改 6 张表 事务耗时 > 100ms、锁等待上升 聚合边界 + 领域事件
③ 循环依赖 改 A 必改 B jdeps/go list 出现环 提取接口 + 依赖倒置
④ 公共模块黑洞 common 有 300 个类 任何改动都触碰 common 限制为纯工具,业务下沉
⑤ 无人敢改 新需求一律 “加 if” 需求评审出现 “这块不能动” 绞杀者式重构(见 28.5 遗留系统集成 (绞杀者 / 防腐层))

代码:把 “环” 找出来

# Go:列出包依赖图,找环
go list -f '{{.ImportPath}} {{join .Imports " "}}' ./... > deps.txt
# 人工或用工具找环
go run github.com/loov/goda@latest graph ./... | head -30

# Java:包级依赖
jdeps -verbose:package build/classes | grep -- "->" | sort | uniq -c | sort -rn | head
# 判据:出现 A -> B 且 B -> A,或某个包被 20+ 包依赖(公共黑洞)

实测:三个可量化信号,一次跑出来

同一示例工程的静态分析输出(对应前面五个阶段中的 ② 与 ④):

信号 修复前 修复后
跨模块内部依赖 1 处 0
模块级循环依赖 1 对(order ↔ payment) 0
最高扇入 user/internal = 2 依赖公开接口后下降

这三个数就是 “泥球程度” 的可观测代理指标:环数、违规数、扇入。它们都能进 CI/看板,也都能向管理层解释 “为什么交付变慢”——不需要 “感觉代码很乱”。

治理顺序:先把环清零(②),再消内部依赖(①),最后压扇入(④)
理由:环让任何局部改动都可能影响全局,是成本最高的一类

反模式

反模式 后果
用 “重构大爆炸” 治理泥球 风险与工期不可控,通常半途而废
只拆目录不拆数据 表仍是公共契约,边界无效
把泥球整体包成微服务 分布式泥球:更难查、更慢
用 “加一层” 掩盖耦合 抽象层自身变成新的耦合点
没有度量就宣称 “在改善” 循环依赖数、变更影响面都没降

| 只统计代码行数/模块数变化 | 泥球可能 “模块变多但耦合更重”,指标失真 | | 治理后不复测 | 没有前后对比,无法证明改善 |

两条铁律

🔑 先立规则、再动结构;没有 CI 规则的重构会被下一次 “赶需求” 打回原形。

🔑 一次只治理一类信号:先把环清零,再处理共享表。

自检

Q1 为什么 “共享数据库” 排在滑坡的第一位?

▸ 答案:因为它是所有后续问题的放大器:表一旦成为公共契约,跨模块事务、循环依赖、公共模块都会随之出现——改表要协调所有使用方,于是大家倾向于 “加字段而不是改结构”,泥球开始堆积。

Q2 “只拆目录不拆数据” 为什么是假重构?

▸ 答案:因为编译期边界可以被绕过(跨 schema 直接查),而数据模型才是运行期的真实耦合。判断重构是否有效,看 “改一张表要动几个模块”,而不是看目录多整齐。

Q3 泥球整体包成微服务会发生什么?

▸ 答案:得到分布式泥球:原来的本地调用变成网络调用,延迟与失败模式变多,但循环依赖与共享数据一个没少——只是从 “编译不过” 变成 “运行时报错”。拆分前必须先理清边界。

Q4 怎么向管理层证明 “泥球在恶化”?

▸ 答案:用三个可量化指标:循环依赖数、单次变更平均影响模块数、回归测试耗时。三者同向上升时,“需求交付变慢” 就有了归因,而不是 “开发不努力”。

Q5 为什么 “环” 一旦出现就会自我强化?

▸ 答案:因为环让 “改 A 必改 B” 成为常态:开发者为了绕开改动,倾向于在两边都 “加一个 if / 加一个字段”,于是环上的耦合越来越深,谁都不敢先动。破环必须先抽接口/事件,把 “双向依赖” 拆成 “单向依赖 + 回调”,这是一次结构性改动,不能靠局部修补。

Q6 “公共模块黑洞” 该怎么治,而不是 “禁止使用 common”?

▸ 答案:把 common 拆成两类:无业务语义的基础设施(错误码、时钟、日志封装)与被误放进去的业务概念(订单状态枚举、金额类型)。前者保留并版本化,后者下沉到真正的业务模块。治完后加一条规则:common 的新增必须评审,且不允许 import 任何业务模块(单向依赖)。

Q7 为什么 “环” 要优先于 “内部依赖” 治理?

▸ 答案:因为内部依赖通常可以用 “换公开接口” 局部修复,而环意味着两个模块互为前提:要打破它必须先抽出第三方的接口/事件,改动面更大,且它让 “任何一处修改都可能影响全局”。先清环,后续的局部重构才有意义。

故障复盘(典型模式,脱敏)

阶段 现象 判断 动作
需求评估 一个 “加个状态” 的需求评估 3 天变 3 周 环让改动扩散到 4 个模块 拆接口
上线 改一处,三处回归失败 共享表 + 隐式依赖 回滚并分批
复盘 环 9 个、最高扇入 12 已进入 “无人敢改” 阶段 先清环
治理后 环 0、扇入降到 4,评估回到 2 天 指标改善可量化 持续看板

根因:泥球不是一次设计事故,而是每季度 “先上线再说” 的累积。代价:交付周期 ×5。根治:把 “环数/违规数/扇入” 纳入季度目标,先清环再谈重构。

关联