代码明明能正常运行,为什么打开文件修改一个功能时,却要到处改动?
只改了一个按钮颜色,支付逻辑却崩了。这样的经历大家多少都有过吧。
用两个词概括这个原因,就是耦合度和内聚性。
耦合度越低越好,内聚性越高越好。优秀设计的八成,都从这句话开始。
本文结合我的经验,讲清这两个概念究竟是什么、为什么被称为软件设计原则的根基,以及如何应用到实际代码中。
先总结一下重点。
- 耦合度:模块之间纠缠得有多紧 → 越低越好
- 内聚性:一个模块中的代码有多专注于一件事 → 越高越好
- 这两个概念是 SOLID、设计模式等知名原则的基础。
- 目标只有一个:“易于修改、易于替换的代码”。
耦合度和内聚性,到底是什么?
先用一句话概括这两个概念:
耦合度描述模块之间的关系,内聚性描述单个模块内部的凝聚程度。
由于方向完全相反,很容易混淆。我是这样记的。
耦合度是“与外部的距离”,内聚性是“内部的聚拢”。
耦合度高时,修改 A 会连带影响 B、C,就像多米诺骨牌一样。
内聚性低时,支付逻辑、邮件发送和日志记录混在同一个类里。只看名字,根本不知道这个类是做什么的。
优秀设计会把两者推向相反方向:对外松散,对内紧密。
为什么这是设计原则的“根基”?
耦合度和内聚性最早在 20 世纪 70 年代的结构化设计理论中得到整理,通常认为由 Larry Constantine 提出。
近半个世纪后依然适用,是因为它们体现的是不受特定语言或潮流影响的本质。
拆解我们熟悉的各种知名原则,最终都会归结到这里。
| 原则·模式 | 归根结底想表达什么 |
|---|---|
| 单一职责原则(SRP) | 提高内聚性 |
| 依赖倒置原则(DIP) | 降低耦合度 |
| 接口隔离原则(ISP) | 切断不必要的耦合 |
| 大多数设计模式 | 低耦合 + 高内聚 |
看到了吧?名称不同,但根基只有一个。
学习新的原则或模式时,我会先问:“这是为了降低耦合度,还是为了提高内聚性?”这样就理解一半了。
如何降低耦合度?从代码来看。
只用语言描述比较抽象,我们来看一个简短的例子。
下面是高耦合代码。Order 类直接知道某个具体的支付公司。
class Order {
let pay = KakaoPay() // 与 Kakao Pay 紧密耦合
func checkout() {
pay.send() // 要换成其他支付方式,就得拆掉这里
}
}
把支付公司换成 Toss 的瞬间,就必须打开 Order 进行修改。这就是高耦合的典型例子。
这次用协议把两者隔开。
class Order {
let pay: Payment // '只依赖“支付”这一约定
init(pay: Payment) { self.pay = pay }
func checkout() { pay.send() } // 传入什么都无所谓
}
现在无论接入哪家支付公司,Order 都不必关心。只需替换实现即可。
在实际工作中这样改完后,即使收到新增支付公司的需求,我也几乎不用动现有代码。这就是低耦合的力量。
如何提高内聚性?
判断内聚性,要看“这个模块是否只做一件事”。
我的标准很简单:说出类名时,如果能自然想到其中的所有方法,就说明内聚性高。
例如,UserService 中混入下面这些内容,就是危险信号。
- 用户注册处理(主要职责)
- 发送营销邮件(别人的工作)
- 生成 CSV 报表(又是别人的工作)
这就像互不相关的工作合租在同一栋房子里。
这时可以让 EmailSender 负责邮件,让 ReportGenerator 负责报表,各自拥有独立的房间。
这样以后修改邮件逻辑时,就不必打开 UserService。变更的影响范围变小了。
低耦合和高内聚最终指向同一个目标:阻止变更扩散。
常见问题(Q&A)
问:耦合度和内聚性,应该先关注哪一个?
我建议先处理内聚性。把模块拆分到各自专注于一项职责后,耦合度通常也会自然得到整理。
问:耦合度一定是 0 才最好吗?
不是。模块完全没有联系,程序就无法运行。目标不是零,而是“只保留必要的联系,并且保持松散”。
问:需要从一开始就设计得完美吗?
没必要。我也会先让程序运行起来,等修改变得痛苦时,再逐步拆掉耦合、提高内聚性。重构本来就是一个反复迭代的过程。
耦合度和内聚性并不是什么华丽的新技术,却是编写持久代码的人共有的习惯。
不妨在今天写的代码中问自己一次:“修改这个之后,影响会扩散到哪里?”不断积累这个问题,设计直觉就会快速提升。加油!

