2. 依赖方向铁律:无环依赖、稳定依赖、抽象依赖

模块依赖方向的控制是架构设计的底层约束。三条铁律——无环依赖原则(ADP)、稳定依赖原则(SDP)、抽象依赖原则(ADP)——共同构成了依赖方向判断的基线。任何违反这些原则的依赖关系,都会在后续演进中引发维护成本飙升、测试困难、部署复杂化等问题。

2.1 三条铁律概述

三条原则并非独立存在,它们分别从图结构合法性、变化容忍度、抽象层级三个维度约束依赖方向。下表对比了每一条原则的核心关注点和判断方法:

原则 英文缩写 核心关注 判断方法 违反后果
无环依赖原则 ADP 依赖图不允许存在环 拓扑排序;静态依赖分析工具 循环编译、连锁变更、死锁风险
稳定依赖原则 SDP 依赖方向必须指向更稳定的模块 计算稳定性指标 I = 出向依赖数 / (入向+出向依赖数) 脆弱模块被高频修改,下游频繁适配
抽象依赖原则 ADP 稳定模块应具有高抽象度 计算抽象度 A = 抽象类与接口数 / 总类型数 稳定模块过于具体,扩展性差

2.2 无环依赖原则(ADP)

2.2.1 含义

模块依赖图必须是一个有向无环图(DAG)。如果从模块A出发可以沿着依赖关系回到模块A,则构成环路。环路意味着模块之间存在循环依赖,任何模块的修改都可能引发环内所有模块的连锁再生效。

2.2.2 判断方法

最直接的方法是在构建系统中启用依赖循环检测:

  • 编译时:使用 dependency-graph 插件分析模块引用关系。
  • 工具检测:JDepend、NDepend、CMake --graphviz 输出 DOT 文件后绘图检查。
  • 人工检查:按模块边界绘制依赖箭头,若存在回路则为无效结构。
反模式案例: 订单模块依赖支付模块,支付模块依赖用户模块,用户模块又依赖订单模块。此时任何模块的接口变更都会波及其他两个模块,导致构建失败或运行时死循环。

2.3 稳定依赖原则(SDP)

2.3.1 含义

依赖方向应指向更稳定的模块。稳定性通过模块的出向依赖与入向依赖的比例衡量。一个模块被越多的其他模块依赖(入向依赖多),它就越稳定;它依赖的模块越多(出向依赖多),它就越不稳定。稳定模块不应依赖不稳定模块。

2.3.2 判断方法

稳定性指标 I = 出向依赖数(Ce) / (入向依赖数(Ca) + 出向依赖数(Ce)):

  • I=0:最高稳定,无出向依赖,只有入向依赖(如基础工具层)。
  • I=1:最低稳定,只有出向依赖,没有入向依赖(如应用入口)。
  • 合理的依赖方向:高I模块依赖低I模块。
实践场景: 领域层(Domain)应保持 I 值接近 0,基础设施层(Infrastructure)的 I 值允许较高,但基础设施层必须依赖领域层,不能反过来。

2.4 抽象依赖原则(ADP)

2.4.1 含义

稳定的模块应该抽象(接口/抽象类),不稳定的模块应该具体(实现类)。抽象依赖原则是对稳定依赖原则的补充:如果某个模块非常稳定(I 值接近 0),但它内部全是具体实现类,那么任何需求变更都需要修改这个模块本身,破坏稳定性。因此,稳定模块应该通过抽象接口来定义契约,具体实现则放在不稳定的模块中。

2.4.2 判断方法

抽象度指标 A = 抽象类型数(Na) / 总类型数(Nc):

  • A 值高:模块大多由接口或抽象类组成,变化点被隔离。
  • A 值低:模块大多为具体实现类。
  • 主序列:将模块的 (I, A) 坐标画在散点图上,越接近主序列线(A + I = 1)说明设计越合理;远离主序列的区域为“痛苦区”或“无用区”。
常见误区: 团队常误以为“抽象模块=接口多”,但如果没有具体实现与之配接,抽象模块自身也无法工作。ADP 强调的是稳定模块使用抽象作为依赖目标,而不是要求所有模块都抽象。

2.5 三条铁律的关系与依赖方向决策流程

实际架构决策时,必须同时满足三条铁律。下一章将详细讲解接口契约的定义方法,但在此之前,必须确保依赖方向已经通过这三条原则的校验。下面通过 SVG 流程图展示依赖方向判断的完整决策路径:

依赖方向铁律决策流程 依赖方向铁律决策流程图 待判断的依赖方向 ① 检查无环依赖(ADP) 有环? 违反 ADP 必须重构消除环 ② 检查稳定依赖(SDP) 方向对? 违反 SDP 反转依赖方向 ③ 检查抽象依赖(ADP) 通过三条铁律

2.6 反模式案例集合

反模式名称 违反原则 典型表现 改造对策
循环依赖环 ADP A→B→C→A 引入接口层或事件总线,切断循环链
脆弱核心 SDP 核心业务模块依赖工具类,而工具类频繁变更 将工具类稳定化,或将依赖方向反转为核心模块定义接口
具体依赖耦合 ADP 高稳定性的基础设施模块直接依赖具体实现类 提取接口,让基础设施模块依赖抽象,实现注入
抽象漂移 ADP 接口数量极多但无稳定实现,或实现模块从不抽象 根据主序列散点图,调整抽象度到合理区间

2.7 实践指南与工具支持

实操检查清单:
  1. 在 CI 流程中集成依赖循环检测(如 JDepend、NDepend、cargo modules)。
  2. 每周运行一次稳定性分析,标记 I 值异常升高的模块。
  3. 对核心域模块(Domain / Entity)强制保持 A ≥ 0.7。
  4. 在架构评审中,用依赖图直观展示当前层的进出向依赖数。