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 知识体系结构图

下面这张图,我画了紧耦合的核心知识体系,方便你整体把握:

紧耦合特征分析 定义 模块间强依赖 内部细节暴露 修改连锁反应 典型特征 修改扩散效应 复用困难 测试成本高 并行开发受限 理解成本高 代码表现 依赖具体实现类 静态方法调用 继承导致耦合 共享全局状态 方法参数过多 度量指标 扇入数 扇出数 耦合度 修改影响范围 解决方案 依赖接口 依赖注入 组合优先 封装参数对象 紧耦合特征分析知识体系结构图

这张图把紧耦合的核心内容串起来了。从定义出发,到典型特征,再到代码层面的具体表现,最后是度量和解决方案。你写代码时,可以对照着这张图自查。

好了,这一章就到这里。紧耦合的特征你心里有数了吧?下一章咱们聊聊松耦合,看看怎么从紧耦合的坑里爬出来。


专注资料整理