什么是耦合(共生),什么是内聚
May 30, 2020
为什么需要理解耦合和内聚?
在架构设计讨论中,“高内聚,低耦合”几乎是所有人的共识。但在实际项目中,我发现很多同学对这两个概念的理解还停留在朴素认知层面:
- 对耦合的误解:认为任何依赖都是不好的,要尽量避免
- 对内聚的模糊:知道要”高内聚”,但不知道如何衡量
- 实践中的困惑:不知道什么是合理的耦合,什么是不合理的耦合
最近在给老板汇报小程序架构演进时,我们一直在讨论”逻辑内聚”的设计,这让我意识到需要对这两个概念有更系统的理解。
核心概念定义
基础定义对比
概念 | 定义 | 关键理解 |
---|---|---|
耦合(Coupling) 共生(Connascence) |
有A、B两个模块,当修改A时,必须修改B,我们称之为B耦合A | • 没有包含关系:耦合是模块间的,不是父子间的 • 编译约束:静态类型语言中可理解为编译器报错的”连接约束” • 与业务无关:不是业务上经常同时修改就叫耦合 |
内聚(Cohesion) | 若干个模块因为某种关系被包含在一起的紧密程度 | • 包含关系:具有父子层级关系 • 耦合占比:内聚 = 子模块间耦合数量 ÷ 子模块总数 • 代码维护视角:关注的是代码组织,不是业务逻辑 |
关系本质
核心理解:内聚是依赖耦合的,都属于模块化的度量指标。我们要防止的不是耦合本身,而是不合理的耦合。
内聚性详解:从弱到强的7个层次
根据《软件架构:架构模式、特征及实践指南》,内聚性有7个层次(按强度从弱到强):
内聚性层次表
层次 | 类型 | 定义 | 特征 | 示例 |
---|---|---|---|---|
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)
原则:倾向于静态共生性,避免动态共生性
- 静态共生性:可以通过IDE工具检测和处理
- 动态共生性:需要运行时分析,更难管理
2. 局部性(Locality)
原则:距离越近,允许的共生性越强
- 同一模块内:可以有较强的共生性
- 跨模块:应该使用较弱的共生性
3. 程度(Degree)
原则:影响范围越小越好
- 影响几个类:可接受
- 影响几十个类:需要重构
实践指南
Page-Jones 的三个指导原则
- 通过模块化拆分,使整体共生性达到最弱
- 最小化跨越模块边界的共生性
- 最大化模块内部的共生性
Jim Weirich 的简化建议
- 程度法则:将强共生性转化为弱共生性
- 局部性法则:距离增加时,使用更弱的共生性
实际操作建议
设计阶段
-
模块边界设计
- 明确模块职责边界
- 最小化跨模块依赖
- 优先使用名称共生性
-
接口设计
- 避免位置耦合,使用命名参数
- 减少算法耦合,抽象通用接口
- 控制类型耦合的传播范围
开发阶段
-
代码组织
- 相关功能聚集在同一模块
- 避免循环依赖
- 清晰的模块导入/导出
-
重构策略
- 识别高耦合点
- 逐步降低共生性强度
- 提高模块内聚性
维护阶段
-
监控指标
- 模块间依赖数量
- 共生性强度分布
- 变更影响范围
-
持续改进
- 定期重构高耦合模块
- 优化模块边界划分
- 提升代码可读性
在前端项目中的应用
组件设计
// 低内聚:功能混杂
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 }); // 名称耦合
}
总结与思考
核心要点
- 耦合不是敌人:合理的耦合是必要的,关键是控制耦合的类型和强度
- 内聚需要耦合:高内聚依赖于合理的内部耦合
- 动态优于静态:静态共生性更容易管理和重构
- 局部性原则:距离决定允许的耦合强度
实践建议
- 优先级排序:名称耦合 > 类型耦合 > 其他形式的耦合
- 模块化设计:明确边界,减少跨模块强耦合
- 工具支持:利用IDE和静态分析工具检测耦合问题
- 持续重构:定期审视和优化模块结构
思考方向
在复杂系统设计中,完全消除耦合是不可能的,也是不必要的。关键是:
- 识别:能够识别不同类型的耦合
- 度量:能够评估耦合的合理性
- 优化:能够有针对性地改进模块结构
最终目标是构建松散耦合、高度内聚的模块化系统,既保证功能的完整性,又维持良好的可维护性。
相关阅读:
参考资料:
Written by xi ming You should follow him on Github