65.9K
CodeProject 正在变化。 阅读更多。
Home

依赖倒置原则、IoC容器和依赖注入:第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (71投票s)

2013年3月12日

CPOL

4分钟阅读

viewsIcon

139949

关于 DIP 及其在真实场景中需求的解释

引言

在开发WPF应用程序时,我遇到了诸如Unity Container、IoC、Dependency Injection之类的术语。当时,我感到困惑,不明白它们为什么需要。但后来,当我逐渐了解了它的好处后,我才意识到它的真正必要性。

在本文中,我将尝试解释DI和IoC的必要性和用法。基本上,本文分为五个部分:

本文的第一部分是关于依赖倒置原则。希望您觉得本文易于理解和实施。

必备组件

最好对以下项目有一些了解:

  • 开闭原则
  • 接口隔离原则

依赖倒置原则(DIP)

DIP是SOLID原则之一,由Robert Martin C.爵士于1992年提出。

根据C. Robert Martin的依赖倒置原则:

  1. 高级模块不应依赖于低级模块。两者都应依赖于抽象。
  2. 抽象不应依赖于细节。细节应依赖于抽象。

DIP指的是将传统的从高层模块到低层模块的依赖关系倒置。

Bob Martin论文中的例子

在图1.a中,复制程序(高层模块)从键盘读取并写入打印机。这里的复制程序依赖于Read KeyboardWrite Printer,并且是紧密耦合的。

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;
        }
    }
}

同样,如果您不断添加更多的读取器或写入器,我们就需要更改复制程序的实现,因为复制程序依赖于读取器和写入器的实现。

为了解决这个问题,我们可以修改我们的copy程序,使其依赖于抽象而不是实现。下图解释了依赖关系的反转。

在上图中,Copy程序依赖于两个抽象IReaderIWriter来执行。只要低层组件符合这些抽象,copy程序就可以从这些组件读取。

例如,在上图中,ReadKeyboard实现了IReader接口,WritePrinter实现了IWriter接口,因此使用IReaderIWriter接口,复制程序就可以执行复制操作。所以,如果我们想添加更多的低层组件,如扫描仪和闪存盘,我们可以通过实现ScannerFlashDisk来实现。以下代码说明了这种情况:

public interface IReader
{
    string Read();
}

public interface IWriter
{
    void Write(string data);
}

public class ReadKeyboard : IReader
{
    public string Read()
    {
        // code to read from keyboard and return as string
    }
}

public class ReadScanner : IReader
{
    public string Read()
    {
        // code to read from scanner and return as string
    }
}

public class WritePrinter : IWriter
{
    public void Write(string data)
    {
        // code to write to the printer
    }
}

public class WriteFlashDisk : IWriter
{
    public void Write(string data)
    {
        // code to write to the flash disk
    }
}

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);
    }
}

突出显示的这段代码替换了实例化读取器和写入器对象的逻辑。在这里,我们将创建控制的逻辑从Copy程序(高层模块)反转到了服务定位器。因此,无需更改copy程序即可添加/删除低层模块。

依赖注入是实现IoC的机制之一。在本系列的后续部分,我将介绍什么是控制反转(IoC),以及使用不同机制实现依赖倒置原则的方法(依赖注入(DI)是其中一种实现方式)。

摘要

在本部分文章中,我解释了依赖倒置原则(DIP)及其在现实场景中的必要性。

在文章的后续部分,我将介绍控制反转(IoC)和依赖注入(DI)。

历史

  • 2020年4月29日,修正了拼写错误
  • 2013年3月12日:首次修订
© . All rights reserved.