在软件开发中,经常遇到一些设计模式和原则,它们帮助构建更加灵活、可维护的系统。其中,依赖注入(Dependency Injection,简称DI)和控制反转(Inversion of Control,简称IoC)是两个非常重要的概念。本文将尝试解释DI和IoC的必要性和用法。文章分为五个部分:
本文是关于依赖倒置原则的。希望能发现本文易于理解并实施。
最好对以下项目有所了解: 单一职责原则 开放/封闭原则 里氏替换原则 接口隔离原则 依赖倒置原则
依赖倒置原则(DIP)是SOLID原则之一,由Robert C. Martin在1992年提出。
S - 单一职责原则 O - 开放/封闭原则 L - 里氏替换原则 I - 接口隔离原则 D - 依赖倒置原则
Robert C. Martin的依赖倒置原则指出: 高层模块不应依赖低层模块。两者都应该依赖于抽象。 抽象不应依赖细节。细节应依赖于抽象。
DIP指的是将传统的从高层模块到低层模块的依赖关系倒置。
例如,假设有一个复制程序(高层模块),它从键盘读取并打印到打印机。在这里,复制程序依赖于ReadKeyboard和WritePrinter,并且紧密耦合。
public class Copy {
public void DoWork() {
ReadKeyboard reader = new ReadKeyboard();
WritePrinter writer = new WritePrinter();
string data = reader.ReadFromKeyboard();
writer.WriteToPrinter(data);
}
}
这种实现似乎完全没问题,直到有添加更多读取器或写入器的要求。在这种情况下,需要更改复制程序以适应新的读取器和写入器,并需要编写一个条件语句,根据使用情况选择读取器和写入器,这违反了面向对象设计的开放/封闭原则。
例如,希望扩展复制程序(见图1.b),它也可以从扫描仪读取并写入到闪存盘。在这种情况下,需要修改复制程序:
public class Copy {
public void DoWork() {
string data;
switch (readerType) {
case "keyboard":
ReadKeyboard reader = new ReadKeyboard();
data = reader.ReadFromKeyboard();
break;
case "scanner":
ReadScanner reader2 = new ReadScanner();
data = reader2.ReadFromScanner();
break;
}
switch (writerType) {
case "printer":
WritePrinter writer = new WritePrinter();
writer.WriteToPrinter(data);
break;
case "flashdisk":
WriteFlashDisk writer2 = new WriteFlashDisk();
writer2.WriteToFlashDisk(data);
break;
}
}
}
类似地,如果继续添加更多的读取器或写入器,需要更改复制程序的实现,因为复制程序依赖于读取器和写入器的实现。
为了解决这个问题,可以修改复制程序,使其依赖于抽象而不是实现。上面的图解释了关于倒置依赖的内容。
在上面的图中,复制程序依赖于两个抽象IReader和IWriter来执行。只要低层组件符合抽象,复制程序就可以从这些组件读取。
例如,在上面的图中,ReadKeyboard实现了IReader接口,WritePrinter实现了IWriter,因此使用IReader和IWriter接口,复制程序可以执行复制操作。因此,如果想添加更多的低层组件,如扫描仪和闪存盘,可以通过实现扫描仪和闪存盘来实现。以下代码说明了这种情况:
public interface IReader {
string Read();
}
public interface IWriter {
void Write(string data);
}
public class ReadKeyboard : IReader {
public string Read() {
// 从键盘读取并返回字符串的代码
}
}
public class ReadScanner : IReader {
public string Read() {
// 从扫描仪读取并返回字符串的代码
}
}
public class WritePrinter : IWriter {
public void Write(string data) {
// 写入打印机的代码
}
}
public class WriteFlashDisk : IWriter {
public void Write(string data) {
// 写入闪存盘的代码
}
}
public class Copy {
private string _readerType;
private string _writerType;
public Copy(string readerType, string writerType) {
_readerType = readerType;
_writerType = writerType;
}
public void DoWork() {
IReader reader;
IWriter writer;
string data;
switch (_readerType) {
case "keyboard":
reader = new ReadKeyboard();
break;
case "scanner":
reader = new ReadScanner();
break;
}
switch (_writerType) {
case "printer":
writer = new WritePrinter();
break;
case "flashdisk":
writer = new WriteFlashDisk();
break;
}
data = reader.Read();
writer.Write(data);
}
}
在这种情况下,细节依赖于抽象,但高层类仍然依赖于低层模块。由于在高层模块的作用域内实例化了低层模块对象,高层模块仍然需要修改新的低层组件,这并不完全满足DIP。
为了消除依赖,需要在高层模块之外创建依赖对象(低层组件),并且应该有一种机制将该依赖对象传递给依赖模块。
现在出现了一个新问题,如何实现依赖倒置。
上述问题的一个答案可能是控制反转(IoC)。考虑以下代码段:
public class Copy {
public void DoWork() {
IReader reader = serviceLocator.GetReader();
IWriter writer = serviceLocator.GetWriter();
string data = reader.Read();
writer.Write(data);
}
}
突出显示的代码替换了实例化读取器和写入器对象的逻辑。在这里,正在将创建从复制程序(高层模块)反转到服务定位器。因此,复制程序不需要随着任何低层模块的添加/移除而更改。
依赖注入是实现IoC的一种机制。在本文的后续部分中,将介绍什么是控制反转(IoC),以及使用不同机制实现依赖倒置原则的方式(依赖注入(DI)是其中一种实现方式)。
在本文的这一部分,已经解释了依赖倒置原则(DIP)及其在实际场景中的必要性。