2. 紧耦合特征分析
好,咱们接着聊。上一章我讲了耦合度的基本概念,这一章咱们深入看看紧耦合这个“反面教材”。
紧耦合,说白了就是模块之间“你中有我,我中有你”,谁也离不开谁。改一个地方,牵一发而动全身。我在早期做项目时,就吃过不少紧耦合的亏。有一次改了一个工具类的日期格式化方法,结果影响了十几个业务模块,测试团队加班了两天才搞定。嗯,从那以后,我对紧耦合就特别敏感。
2.1 紧耦合的定义
紧耦合(Tight Coupling)是指两个或多个模块之间存在高度的依赖关系。一个模块的内部实现细节,被另一个模块直接依赖。这种依赖关系使得模块之间难以独立开发、测试和维护。
你想想看,如果A模块直接调用了B模块的内部变量,那B模块一改,A模块就得跟着改。这就是典型的紧耦合。
核心定义:紧耦合 = 模块间存在强依赖 + 内部细节暴露 + 修改连锁反应
2.2 典型特征
我总结了紧耦合的几个典型特征,你在代码审查时可以对照着看:
- 修改扩散效应:改一个模块,需要同步修改多个模块。我曾经在一个遗留系统中,改一个数据库字段名,结果改了20多个文件。
- 复用困难:想复用某个模块,必须连带着把它的依赖模块一起搬过去。说白了就是“买一送N”,谁受得了?
- 测试成本高:单元测试时,需要mock大量的依赖对象。有时候mock的代码比测试代码还长。
- 并行开发受限:两个开发人员无法同时开发两个紧耦合的模块,必须等一个人做完,另一个人才能开始。
- 理解成本高:看代码时,需要同时理解多个模块的上下文。脑子不够用啊。
2.3 代码层面的表现
理论说完了,咱们看代码。紧耦合在代码层面有几种常见表现,我一个个说。
2.3.1 直接依赖具体实现类
这是最常见的。代码里到处都是 new XXX(),而不是依赖接口。
// 紧耦合:直接依赖具体实现
public class OrderService {
private EmailSender emailSender = new SmtpEmailSender();
public void processOrder(Order order) {
// 处理订单逻辑
emailSender.send(order.getEmail(), "订单已处理");
}
}
// 如果哪天想换成阿里云邮件服务,就得改 OrderService 的代码
我个人习惯是,能依赖接口就绝不依赖具体类。这样换实现的时候,只需要改一行配置,而不是改业务代码。
2.3.2 静态方法调用
静态方法看着方便,但用多了就是灾难。尤其是那些工具类的静态方法,一旦改了签名,所有调用方都得改。
// 紧耦合:到处调用静态方法
public class PaymentService {
public boolean pay(Order order) {
// 直接调用静态方法
return PaymentUtils.process(order.getAmount(), order.getCurrency());
}
}
// 如果 PaymentUtils.process 方法签名变了,所有调用方都得改
避坑指南:我曾经在一个项目中,大量使用静态工具类。后来业务调整,需要支持多种支付渠道,结果静态方法没法做多态,只能硬着头皮重构。那叫一个痛苦。
2.3.3 继承导致的紧耦合
继承是面向对象的重要特性,但滥用继承会导致子类和父类高度耦合。子类知道父类的所有内部细节。
// 紧耦合:继承导致子类依赖父类内部实现
public class BaseDao {
protected Connection connection;
protected void openConnection() {
// 建立数据库连接
}
}
public class UserDao extends BaseDao {
public User findById(Long id) {
openConnection(); // 依赖父类的内部方法
// 查询逻辑
}
}
// 如果 BaseDao 改了连接方式,UserDao 也得跟着改
我建议优先使用组合而不是继承。组合的耦合度通常比继承低得多。
2.3.4 共享全局状态
全局变量、单例模式、静态集合,这些都是紧耦合的温床。模块之间通过共享状态通信,改一个地方,所有地方都可能受影响。
// 紧耦合:共享全局状态
public class GlobalCache {
public static Map<String, Object> cache = new HashMap<>();
}
public class UserService {
public User getUser(String id) {
Object obj = GlobalCache.cache.get("user_" + id);
// 直接操作全局缓存
}
}
public class OrderService {
public void clearUserCache(String userId) {
GlobalCache.cache.remove("user_" + userId);
// 另一个模块也在操作同一个缓存
}
}
小技巧:如果你发现代码里有很多 public static 的变量,或者到处都在用同一个单例,那就要警惕了。这往往是紧耦合的信号。
2.3.5 方法参数过多
一个方法有七八个参数,甚至十几个参数,这通常意味着调用方需要知道太多内部细节。
// 紧耦合:参数过多,调用方需要知道太多细节
public void createOrder(String userId, String productId, int quantity,
String address, String phone, String paymentMethod,
String couponId, String remark, boolean needInvoice) {
// 创建订单逻辑
}
// 调用方必须按顺序传入所有参数,少一个都不行
参数过多时,我通常会考虑封装成参数对象。这样调用方只需要关心参数对象的结构,而不是每个参数的顺序和类型。
2.4 紧耦合的度量
怎么量化紧耦合?我常用几个指标:
| 指标 | 说明 | 紧耦合表现 |
|---|---|---|
| 扇入数 | 一个模块被多少个其他模块调用 | 扇入数过高,改一个模块影响面大 |
| 扇出数 | 一个模块调用了多少个其他模块 | 扇出数过高,模块依赖太多外部模块 |
| 耦合度 | 模块间依赖的紧密程度 | 直接依赖具体类 > 依赖接口 |
| 修改影响范围 | 修改一个模块需要改动的文件数 | 影响范围越大,耦合越紧 |
2.5 知识体系结构图
下面这张图,我画了紧耦合的核心知识体系,方便你整体把握:
这张图把紧耦合的核心内容串起来了。从定义出发,到典型特征,再到代码层面的具体表现,最后是度量和解决方案。你写代码时,可以对照着这张图自查。
好了,这一章就到这里。紧耦合的特征你心里有数了吧?下一章咱们聊聊松耦合,看看怎么从紧耦合的坑里爬出来。