原文:之前@有个梨UGle… - @leonlucc的微博 - 微博 (weibo.com)


之前 @有个梨UGlee 发起了一个关于松耦合/紧耦合标准的大讨论,我当时提了一个观点是耦合度是对跨边界依赖的复杂度的描述。这里再对边界和依赖两个概念补充一下。

首先对边界的三种级别做一个阐述:

  1. 源码级
    用命名空间、包或者类来区分不同的组件,组件运行于同一进程,边界属于一种源码级别白盒形式。跨边界的方式为直接调用其他组件的方法,迅速而廉价。是耦合度比较高的边界,如果所依赖组件出现问题,将无法通过编译。

  2. lib 包级
    用二进制或字节码形式的 lib 包作为组件的载体,组件运行于同一进程,边界外对边界内是黑盒。跨边界的方式通常为接口调用,但需要静态或者动态链接。静态链接在编译期完成,耦合度较高,所依赖组件出现问题同样无法通过编译。动态链接则耦合度适中,所依赖组件出现问题会影响部分逻辑的运行。

  3. 进程级
    组件以进程的方式独立部署,边界外对边界内是黑盒,且完全独立。跨边界的方式为进程通信,有共享资源、事件消息、服务调用等或同步或异步的手段,通信成本和延时都较高。是耦合度最低的边界,要是细分的话,异步通信比同步通信耦合度更低。

有了边界,那么依赖就是系统中的跨边界调用。bob大叔在《Clean Architecture》中提到了和组件依赖关系有关的两个原则:

  1. 无依赖环原则 (THE ACYCLIC DEPENDENCIES PRINCIPLE)
    即组件依赖关系图中不应该出现环。显然,系统中存在循环依赖的组件会增加耦合度。

  2. 稳定依赖原则 (THE STABLE DEPENDENCIES PRINCIPLE)
    即依赖关系必须要指向更稳定的方向。一个组件的不稳定性可以用扇出公式结算:I = Fan-out / (Fan-in + Fan-out),对外依赖越多则越不稳定。
    如果考虑单个组件的耦合度,那这个公式值得参考,虽然耦合度并不等同于不稳定性,出度为0也不代表耦合度为0,但至少对依赖的稳定性做出了量化。
    如果要计算系统整体的耦合度,这个公式就不太适用,还得从组间两两之间的耦合度入手,一个简单的思路是,和所有两两组间依赖关系的数量和不稳定性成正相关。

    综上,如果要对耦合度做一个数学化的标准,可以尝试对边界的复杂度和依赖的复杂度进行量化,从而可以为解耦优化提供一个参考指标。

| 边界级别 | 特征 | 跨边界方式 | 跨边界通信 | 案例 | 耦合度 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 源码级 | 用命名空间、包或者类来区分不同的组件,组件运行于同一进程,边界属于白盒形式 | 方法调用 | 高通信量,低延时 | 所有直接的类方法和函数调用 | 高 | | lib 包级 | 用二进制或字节码形式的 lib 包作为组件的载体,组件运行于同一进程,边界外对边界内是黑盒 | 接口调用,需要编译期或运行期的链接 | 中通信量,低延时 | 1. 编译型语言的开发包
2. 编译型语言的动态链接库,解释型语言的模块或开发包 | 中 | |进程级 | 组件以进程的方式独立部署,边界外对边界内是黑盒,且完全独立 | 同步进程通信:服务调用
异步进程通信:共享资源、事件信息 | 低通信量,高延时 | 1. 微服务 API
2. 信号量、消息队列 | 低 |

依赖原则解释耦合度
无环依赖组件依赖关系图中不应该出现环与循环依赖的数量成正相关
稳定依赖依赖关系必须要指向更稳定的方向与依赖的数量和不稳定性成正相关