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

📅
1 分钟阅读
·

耦合和内聚的适用范围

架构设计常以“高内聚、低耦合”为目标,但实际讨论中,这两个概念的边界经常不清楚:

  • 将所有依赖都视为应避免的耦合。
  • 知道要“高内聚”,但缺少衡量方式。
  • 无法判断某个耦合是否会带来额外的变更成本。

在讨论小程序架构演进中的“逻辑内聚”时,需要先明确这两个概念的定义和度量范围。

概念定义

基础定义

概念定义说明
耦合(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 }); // 名称耦合
}

结论

模块之间需要通过接口、类型和数据协议协作,因此耦合无法被完全消除。应优先识别跨模块的动态共生、位置共生和算法共生,并把影响范围限制在明确的模块边界内。模块内部可以保留服务同一职责所需的关联;跨模块接口则应减少隐含顺序、时间和共享值约束。


相关阅读

参考资料


315 字 · 104 段落
ximing

Follow onGitHub

相关文章