什么是耦合(共生),什么是内聚

May 30, 2020

为什么需要理解耦合和内聚?

在架构设计讨论中,“高内聚,低耦合”几乎是所有人的共识。但在实际项目中,我发现很多同学对这两个概念的理解还停留在朴素认知层面:

  • 对耦合的误解:认为任何依赖都是不好的,要尽量避免
  • 对内聚的模糊:知道要”高内聚”,但不知道如何衡量
  • 实践中的困惑:不知道什么是合理的耦合,什么是不合理的耦合

最近在给老板汇报小程序架构演进时,我们一直在讨论”逻辑内聚”的设计,这让我意识到需要对这两个概念有更系统的理解。

核心概念定义

基础定义对比

概念 定义 关键理解
耦合(Coupling)
共生(Connascence)
有A、B两个模块,当修改A时,必须修改B,我们称之为B耦合A 没有包含关系:耦合是模块间的,不是父子间的
编译约束:静态类型语言中可理解为编译器报错的”连接约束”
与业务无关:不是业务上经常同时修改就叫耦合
内聚(Cohesion) 若干个模块因为某种关系被包含在一起的紧密程度 包含关系:具有父子层级关系
耦合占比:内聚 = 子模块间耦合数量 ÷ 子模块总数
代码维护视角:关注的是代码组织,不是业务逻辑

关系本质

核心理解:内聚是依赖耦合的,都属于模块化的度量指标。我们要防止的不是耦合本身,而是不合理的耦合

内聚性详解:从弱到强的7个层次

根据《软件架构:架构模式、特征及实践指南》,内聚性有7个层次(按强度从弱到强):

e95c2a33-03a5-4dac-ac5d-2d5d73a940d7

内聚性层次表

层次 类型 定义 特征 示例
1 偶然内聚 模块内元素间没有任何联系 最低内聚度,巧合组合 工具类大杂烩
2 逻辑内聚 相关功能组合,通过参数选择 功能相关但实现独立 各种验证函数放在一起
3 时间内聚 需要同时执行的动作组合 时间相关性 初始化模块
4 过程内聚 按特定顺序执行的处理元素 顺序相关但无数据传递 操作流程步骤
5 通信内聚 操作同一数据结构的元素 数据相关性 用户信息CRUD
6 顺序内聚 前一个输出是后一个输入 数据流相关性 数据处理管道
7 功能内聚 所有元素为完成同一功能存在 最高内聚度,单一职责 计算水费模块

内聚性改进示例

低内聚的代码

class Module {
  a = 1;
  b = 2;
  
  fn1() {
    this.a = 1;
  }
  
  fn2() {
    return this.b;
  }
  
  fn3() {
    console.log('fn');
  }
}

改进后的高内聚代码

class ModuleA {
  a = 1;
  
  fn1() {
    this.a = 1;
  }
}

class ModuleB {
  b = 3;
  
  fn2() {
    return this.b;
  }
}

class ModuleC {
  fn3() {
    console.log('fn');
  }
}

共生性(Connascence):耦合的精确度量

1996年 Meilir Page-Jones 在《What Every Programmer Should Know About Object-Oriented Design》中完善了耦合的度量,命名为”Connascence”:

如果一个组件的改变会要求另一个组件进行修改,才能保持系统的整体正确性,那么这两个组件就是共生的。

静态共生性(Static Connascence)

1. 名称共生性(Connascence of Name, CoN)

特征:引用相同的名称

class Customer {
  Age = 25; // 第3行
}

const customer = new Customer();
console.log(customer.Age); // 第8行 - 如果第3行改名,这里必须同步修改

评价:最理想的共生类型,现代IDE可以自动处理重命名。

2. 类型共生性(Connascence of Type, CoT)

特征:必须就数据类型达成一致

function sayHello(name: string): string {
  return `Hello ${name}`;
}

sayHello(123); // 错误:类型不匹配

3. 意义共生性(Connascence of Meaning, CoM)

特征:必须就特定值的含义达成一致(Magic Number)

// 不好的例子
if (status === 1) { /* 激活 */ }
if (status === 2) { /* 禁用 */ }

// 改进
const STATUS = {
  ACTIVE: 1,
  DISABLED: 2
};
if (status === STATUS.ACTIVE) { /* 激活 */ }

4. 位置共生性(Connascence of Position, CoP)

特征:必须就顺序达成一致

// 位置耦合
function createUser(firstName, lastName, address) {
  // ...
}
createUser("Bob", "Marley", "Jamaica");

// 改进:名称耦合
function createUser(userInfo) {
  const { firstName, lastName, address } = userInfo;
  // ...
}
createUser({
  firstName: "Bob",
  lastName: "Marley", 
  address: "Jamaica"
});

5. 算法共生性(Connascence of Algorithm, CoA)

特征:必须就算法达成一致

// 客户端和服务端必须使用相同的加密算法
const clientHash = md5(password + salt);
const serverHash = md5(password + salt); // 必须保持一致

动态共生性(Dynamic Connascence)

1. 执行共生性(Connascence of Execution, CoE)

特征:代码执行顺序的耦合

// 错误的顺序
email = new Email();
email.setRecipient("foo@example.com");
email.setSender("me@me.com");
email.send(); // 先发送了
email.setSubject("whoops"); // 后设置主题

// 正确的顺序
email = new Email();
email.setRecipient("foo@example.com");
email.setSender("me@me.com");
email.setSubject("Important Message");
email.send();

2. 时间共生性(Connascence of Timing, CoT)

特征:执行时间上的耦合

// Bootstrap Modal 的时间耦合问题
$(element).modal('hide');
$(element).modal('show'); // 错误!动画未完成

// 正确的做法
$(element).modal('hide');
$(element).on('hidden.bs.modal', () => {
  $(element).modal('show'); // 等待动画完成
});

3. 值共生性(Connascence of Values, CoV)

特征:必须同时更改多个值

// 测试中的值耦合
function calculateTotal() {
  return 50; // 函数返回值
}

// 测试代码
expect(calculateTotal()).toBe(50); // 必须保持一致

4. 身份共生性(Connascence of Identity, CoI)

特征:必须引用同一个对象实例

// 问题代码:对象副本导致的身份耦合
function changeMovieTitle(movie, newTitle) {
  const updatedMovie = { ...movie, title: newTitle };
  return updatedMovie; // 返回新对象
}

function displayMovie(movie) {
  console.log(movie.title); // 显示旧标题
}

// 解决方案:返回对象ID而不是对象本身
function changeMovieTitle(movieId, newTitle) {
  // 直接修改数据库
  database.updateMovie(movieId, { title: newTitle });
  return movieId;
}

共生性的三个属性

1. 强度(Strength)

1745476869659

原则:倾向于静态共生性,避免动态共生性

  • 静态共生性:可以通过IDE工具检测和处理
  • 动态共生性:需要运行时分析,更难管理

2. 局部性(Locality)

原则:距离越近,允许的共生性越强

  • 同一模块内:可以有较强的共生性
  • 跨模块:应该使用较弱的共生性

3. 程度(Degree)

原则:影响范围越小越好

  • 影响几个类:可接受
  • 影响几十个类:需要重构

实践指南

Page-Jones 的三个指导原则

  1. 通过模块化拆分,使整体共生性达到最弱
  2. 最小化跨越模块边界的共生性
  3. 最大化模块内部的共生性

Jim Weirich 的简化建议

  1. 程度法则:将强共生性转化为弱共生性
  2. 局部性法则:距离增加时,使用更弱的共生性

实际操作建议

设计阶段

  1. 模块边界设计

    • 明确模块职责边界
    • 最小化跨模块依赖
    • 优先使用名称共生性
  2. 接口设计

    • 避免位置耦合,使用命名参数
    • 减少算法耦合,抽象通用接口
    • 控制类型耦合的传播范围

开发阶段

  1. 代码组织

    • 相关功能聚集在同一模块
    • 避免循环依赖
    • 清晰的模块导入/导出
  2. 重构策略

    • 识别高耦合点
    • 逐步降低共生性强度
    • 提高模块内聚性

维护阶段

  1. 监控指标

    • 模块间依赖数量
    • 共生性强度分布
    • 变更影响范围
  2. 持续改进

    • 定期重构高耦合模块
    • 优化模块边界划分
    • 提升代码可读性

在前端项目中的应用

组件设计

// 低内聚:功能混杂
class UserComponent {
  renderProfile() { /* 渲染用户信息 */ }
  validateForm() { /* 表单验证 */ }
  sendEmail() { /* 发送邮件 */ }
  calculateAge() { /* 计算年龄 */ }
}

// 高内聚:单一职责
class UserProfile {
  renderProfile() { /* 只负责渲染 */ }
}

class UserValidator {
  validateForm() { /* 只负责验证 */ }
}

class EmailService {
  sendEmail() { /* 只负责邮件 */ }
}

状态管理

// 强耦合:直接修改全局状态
function updateUser(userId, data) {
  window.globalState.users[userId] = data; // 位置耦合
  window.globalState.lastUpdate = Date.now(); // 值耦合
}

// 弱耦合:通过接口操作
function updateUser(userId, data) {
  userStore.update(userId, data); // 名称耦合
  eventBus.emit('user:updated', { userId, data }); // 名称耦合
}

总结与思考

核心要点

  1. 耦合不是敌人:合理的耦合是必要的,关键是控制耦合的类型和强度
  2. 内聚需要耦合:高内聚依赖于合理的内部耦合
  3. 动态优于静态:静态共生性更容易管理和重构
  4. 局部性原则:距离决定允许的耦合强度

实践建议

  1. 优先级排序:名称耦合 > 类型耦合 > 其他形式的耦合
  2. 模块化设计:明确边界,减少跨模块强耦合
  3. 工具支持:利用IDE和静态分析工具检测耦合问题
  4. 持续重构:定期审视和优化模块结构

思考方向

在复杂系统设计中,完全消除耦合是不可能的,也是不必要的。关键是:

  • 识别:能够识别不同类型的耦合
  • 度量:能够评估耦合的合理性
  • 优化:能够有针对性地改进模块结构

最终目标是构建松散耦合、高度内聚的模块化系统,既保证功能的完整性,又维持良好的可维护性。


相关阅读

参考资料