1. 引言

本文将深入探讨Java中的耦合机制,包括不同类型的耦合及其特点。最后我们会简要介绍依赖倒置原则和控制反转,并说明它们与耦合的关系。

2. Java中的耦合

耦合描述的是系统中类之间相互依赖的程度。在开发过程中,我们的目标是降低耦合度。

假设我们要设计一个元数据收集器应用。这个应用负责收集元数据:获取XML格式的元数据,然后导出为CSV文件。初始设计可能是这样的:

元数据收集器紧耦合设计图

这个模块同时负责获取、处理和导出数据。但这是个糟糕的设计,违反了单一职责原则。为了改进,我们需要分离关注点:

元数据收集器关注点分离设计图

现在设计解耦为两个新模块:XML获取和CSV导出。元数据收集器模块同时依赖这两个模块。这个设计比初始版本好,但仍有改进空间。接下来我们将看到如何通过良好的耦合实践进一步优化设计。

3. 紧耦合

当一组类高度相互依赖,或者某些类承担过多职责时,就形成了紧耦合。另一种常见情况是对象直接创建其他对象供自己使用。紧耦合代码难以维护。

让我们用基础应用来观察这种行为。先看代码定义。首先是XMLFetch类:

public class XMLFetch {
    public List<Object> fetchMetadata() {
        List<Object> metadata = new ArrayList<>();
        // 执行一些操作
        return metadata;
    }
}

然后是CSVExport类:

public class CSVExport {
    public File export(List<Object> metadata) {
        System.out.println("Exporting data...");
        // 导出元数据
        File outputCSV = null;
        return outputCSV;
    }
}

最后是MetadataCollector类:

public class MetadataCollector {
    private XMLFetch xmlFetch = new XMLFetch();
    private CSVExport csvExport = new CSVExport();
    public void collectMetadata() {
        List<Object> metadata = xmlFetch.fetchMetadata();
        csvExport.export(metadata);
    }
}

可以看到,MetadataCollector类直接依赖XMLFetchCSVExport类,并且负责创建它们的实例。

如果要改进收集器,比如添加JSON数据获取和PDF导出功能,我们需要在类中添加这些新元素。看看"改进"后的代码:

public class MetadataCollector {
    ...
    private CSVExport csvExport = new CSVExport();
    private PDFExport pdfExport = new PDFExport();
    public void collectMetadata(int inputType, int outputType) {
        if (outputType == 1) {
            List<Object> metadata = null;
            if (inputType == 1) {
                metadata = xmlFetch.fetchMetadata();
            } else {
                metadata = jsonFetch.fetchMetadata();
            }
            csvExport.export(metadata);
        } else {
            List<Object> metadata = null;
            if (inputType == 1) {
                metadata = xmlFetch.fetchMetadata();
            } else {
                metadata = jsonFetch.fetchMetadata();
            }
            pdfExport.export(metadata);
        }
    }
}

元数据收集器模块需要使用标志位来处理新功能。根据标志值实例化相应的子模块。但每个新功能不仅使代码更复杂,还增加了维护难度。这就是典型的紧耦合特征,必须避免。

4. 松耦合

开发过程中,类之间的关系数量应该尽可能少,这就是松耦合。松耦合是指对象从外部获取要使用的对象。对象之间相互独立。松耦合代码能降低维护成本,同时提高系统灵活性。

松耦合通过依赖倒置原则实现,下一节我们将详细说明。

5. 依赖倒置原则

依赖倒置原则(DIP)指出高层模块不应依赖低层模块两者都应依赖抽象

当前设计的核心问题就在这里:元数据收集器(高层模块)直接依赖XML获取和CSV导出(低层模块)。

如何改进设计?DIP给出了解决方向,但未说明具体实现。这时控制反转(IoC)就派上用场了。IoC定义了模块间抽象的建立方式,简单说就是DIP的实现手段。

让我们将DIP和IoC应用到当前示例。首先定义获取和导出数据的接口:

public interface FetchMetadata {
    List<Object> fetchMetadata();
}

很简单吧?现在定义导出接口:

public interface ExportMetadata {
    File export(List<Object> metadata);
}

接下来需要让相应类实现这些接口。更新现有类:

public class XMLFetch implements FetchMetadata {
    @Override
    public List<Object> fetchMetadata() {
        List<Object> metadata = new ArrayList<>();
        // 执行一些操作
        return metadata;
    }
}

更新CSVExport类:

public class CSVExport implements ExportMetadata {
    @Override
    public File export(List<Object> metadata) {
        System.out.println("Exporting data...");
        // 导出元数据
        File outputCSV = null;
        return outputCSV;
    }
}

最后更新主模块代码以支持新设计:

public class MetadataCollector {
    private FetchMetadata fetchMetadata;
    private ExportMetadata exportMetadata;
    public MetadataCollector(FetchMetadata fetchMetadata, ExportMetadata exportMetadata) {
        this.fetchMetadata = fetchMetadata;
        this.exportMetadata = exportMetadata;
    }
    public void collectMetadata() {
        List<Object> metadata = fetchMetadata.fetchMetadata();
        exportMetadata.export(metadata);
    }
}

代码有两个主要变化:

  1. 类只依赖抽象接口,不再依赖具体类型
  2. 移除了对低层模块的直接依赖,收集器模块不再需要创建低层模块实例

通过标准接口与低层模块交互。这种设计的优势在于:添加新的获取/导出模块时,收集器代码完全不需要修改。

通过应用DIP和IoC,我们显著改善了系统设计。通过反转控制,应用变得解耦、可测试、可扩展且易维护。当前设计如下图所示:

应用DIP和IoC的元数据收集器设计图

最终我们通过DIP+IoC消除了所有紧耦合代码,用松耦合代码优化了初始设计。

6. 结论

本文深入探讨了Java中的耦合机制。我们首先了解了耦合的基本概念,然后对比了紧耦合与松耦合的区别。接着通过设计示例学习了如何应用DIP和IoC实现松耦合。每一步都展示了良好设计模式带来的代码改进。

完整代码示例可在GitHub获取。


原始标题:Coupling in Java | Baeldung