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
类直接依赖XMLFetch
和CSVExport
类,并且负责创建它们的实例。
如果要改进收集器,比如添加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);
}
}
代码有两个主要变化:
- 类只依赖抽象接口,不再依赖具体类型
- 移除了对低层模块的直接依赖,收集器模块不再需要创建低层模块实例
通过标准接口与低层模块交互。这种设计的优势在于:添加新的获取/导出模块时,收集器代码完全不需要修改。
通过应用DIP和IoC,我们显著改善了系统设计。通过反转控制,应用变得解耦、可测试、可扩展且易维护。当前设计如下图所示:
最终我们通过DIP+IoC消除了所有紧耦合代码,用松耦合代码优化了初始设计。
6. 结论
本文深入探讨了Java中的耦合机制。我们首先了解了耦合的基本概念,然后对比了紧耦合与松耦合的区别。接着通过设计示例学习了如何应用DIP和IoC实现松耦合。每一步都展示了良好设计模式带来的代码改进。
完整代码示例可在GitHub获取。