已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,

什么是依赖注入,何时/为什么应该或不应该使用它?


当前回答

依赖注入是解决“依赖混淆”需求的一种可能方案。依赖性混淆是一种将“明显”性质从向需要依赖性的类提供依赖性的过程中去除的方法,因此在某种程度上混淆了向所述类提供所述依赖性。这不一定是坏事。事实上,通过混淆向类提供依赖项的方式,类外部的某个东西负责创建依赖项,这意味着在各种情况下,可以向类提供不同的依赖项实现,而不需要对类进行任何更改。这对于在生产和测试模式之间切换非常有用(例如,使用“模拟”服务依赖)。

不幸的是,糟糕的部分是,有些人认为你需要一个专门的框架来进行依赖性混淆,如果你选择不使用特定的框架来做,那么你在某种程度上就是一个“低级”程序员。另一个非常令人不安的神话是,依赖性注入是实现依赖性混淆的唯一方法。这显然是历史性的,显然是100%错误的,但你很难说服一些人,依赖项注入可以替代依赖项混淆需求。

多年来,程序员们已经了解了依赖性混淆的需求,在考虑依赖性注入之前和之后,许多替代解决方案都已经发展起来。有工厂模式,但也有许多使用ThreadLocal的选项,其中不需要对特定实例进行注入-依赖关系被有效地注入到线程中,这样做的好处是使对象(通过方便的静态getter方法)可用于任何需要它的类,而无需向需要它的类别添加注释并设置复杂的XML“粘合”以实现这一点。当持久性需要依赖项(JPA/JDO或其他)时,它允许您更容易地实现“跨持久性”,并且域模型和业务模型类完全由POJO组成(即没有特定于框架的/锁定在注释中的)。

其他回答

什么是依赖注入(DI)?

正如其他人所说,依赖注入(DI)消除了直接创建和管理我们感兴趣的类(消费者类)所依赖的其他对象实例(在UML意义上)的生命周期的责任。这些实例通常作为构造函数参数或通过属性设置器传递给我们的消费者类(依赖对象实例化和传递给消费者类的管理通常由控制反转(IoC)容器执行,但这是另一个主题)。

DI、DIP和固体

具体来说,在Robert C Martin的面向对象设计的SOLID原则的范例中,DI是依赖反转原则(DIP)的可能实现之一。DIP是SOLID咒语的D——其他DIP实现包括服务定位器和插件模式。

DIP的目标是解耦类之间紧密、具体的依赖关系,相反,通过抽象来放松耦合,这可以通过接口、抽象类或纯虚拟类来实现,具体取决于所使用的语言和方法。

如果没有DIP,我们的代码(我称之为“消费类”)直接耦合到一个具体的依赖项,并且经常承担着知道如何获取和管理该依赖项实例的责任,即概念上:

"I need to create/use a Foo and invoke method `GetBar()`"

然而,在应用DIP后,这一要求被放宽,获得和管理Foo依赖寿命的担忧已经消除:

"I need to invoke something which offers `GetBar()`"

为什么使用DIP(和DI)?

以这种方式解耦类之间的依赖性允许用其他实现轻松替换这些依赖性类,这些实现也满足抽象的前提条件(例如,依赖性可以与同一接口的另一个实现切换)。此外,正如其他人所提到的,通过DIP解耦类的最常见原因可能是允许单独测试消费类,因为这些依赖关系现在可以被清除和/或嘲笑。

DI的一个结果是依赖对象实例的寿命管理不再由消费类控制,因为依赖对象现在被传递到消费类(通过构造函数或setter注入)。

这可以用不同的方式来看待:

如果需要保留消费类对依赖项的生命周期控制,则可以通过将用于创建依赖类实例的(抽象)工厂注入消费类来重新建立控制。消费者将能够根据需要通过工厂上的Create获取实例,并在完成后处理这些实例。或者,依赖实例的生命周期控制可以放弃给IoC容器(下面将详细介绍)。

何时使用DI?

在可能需要用依赖性替代等效实现的情况下,任何时候,如果您需要对类的方法进行单元测试,依赖项的生命周期的不确定性可能需要进行实验(例如,嘿,MyDepClass是线程安全的-如果我们将其设置为单例并将同一实例注入所有消费者,会怎么样?)

实例

这里是一个简单的C#实现。给定以下消费类:

public class MyLogger
{
   public void LogRecord(string somethingToLog)
   {
      Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
   }
}

虽然看似无害,但它对另外两个类System.DateTime和System.Console有两个静态依赖关系,这不仅限制了日志输出选项(如果没有人在监视,则日志记录到控制台将毫无价值),而且更糟糕的是,考虑到对非确定性系统时钟的依赖关系,很难自动测试。

然而,我们可以将DIP应用于这个类,方法是将时间戳问题抽象为依赖项,并将MyLogger仅耦合到一个简单的接口:

public interface IClock
{
    DateTime Now { get; }
}

我们还可以将对Console的依赖放宽为抽象,例如TextWriter。依赖注入通常实现为构造函数注入(将抽象作为参数传递给依赖项作为消费类的构造函数)或Setter注入(通过setXyz()Setter或定义了{set;}的.Net Property传递依赖项)。构造函数注入是首选的,因为这样可以保证类在构造后处于正确的状态,并允许将内部依赖字段标记为只读(C#)或最终(Java)。因此,在上面的示例中使用构造函数注入,这就给我们留下了:

public class MyLogger : ILogger // Others will depend on our logger.
{
    private readonly TextWriter _output;
    private readonly IClock _clock;

    // Dependencies are injected through the constructor
    public MyLogger(TextWriter stream, IClock clock)
    {
        _output = stream;
        _clock = clock;
    }

    public void LogRecord(string somethingToLog)
    {
        // We can now use our dependencies through the abstraction 
        // and without knowledge of the lifespans of the dependencies
        _output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
    }
}

(需要提供一个具体的Clock,它当然可以恢复到DateTime。现在,这两个依赖关系需要由IoC容器通过构造函数注入提供)

可以构建一个自动化的单元测试,这无疑证明了我们的记录器工作正常,因为我们现在可以控制依赖关系-时间,我们可以监视书面输出:

[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
    // Arrange
    var mockClock = new Mock<IClock>();
    mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
    var fakeConsole = new StringWriter();

    // Act
    new MyLogger(fakeConsole, mockClock.Object)
        .LogRecord("Foo");

    // Assert
    Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}

下一步

依赖注入总是与控制反转容器(IoC)相关联,以注入(提供)具体的依赖实例,并管理生命周期实例。在配置/引导过程中,IoC容器允许定义以下内容:

每个抽象和配置的具体实现之间的映射(例如“消费者请求IBar时,返回ConcreteBar实例”)可以为每个依赖项的生命周期管理设置策略,例如为每个消费者实例创建新对象,在所有消费者之间共享单一依赖项实例,仅在同一线程之间共享同一依赖项实例等。在.Net中,IoC容器了解IDisposable等协议,并将根据配置的生命周期管理来负责处理依赖关系。

通常,一旦IoC容器被配置/引导,它们就可以在后台无缝地运行,从而让编码器专注于手头的代码,而不用担心依赖性。

DI友好代码的关键是避免类的静态耦合,并且不要使用new()创建依赖项

根据上面的示例,依赖关系的解耦确实需要一些设计工作,对于开发人员来说,需要进行范式转换,以打破直接添加依赖关系的习惯,转而信任容器来管理依赖关系。

但好处很多,特别是能够彻底测试你感兴趣的班级。

注意:POCO/POJO/Serialization DTO/Entity Graphs/Anonymous JSON投影等(即“仅数据”类或记录)的创建/映射/投影(通过新的..())不被视为依赖项(在UML意义上),也不受DI的约束。使用new来投射这些是很好的。

公认的答案是一个好答案——但我想补充一点,DI非常像代码中避免硬编码常量的经典做法。

当您使用诸如数据库名称之类的常量时,您可以将其从代码内部快速移动到某个配置文件,并将包含该值的变量传递到需要它的位置。这样做的原因是,这些常量通常比代码的其他部分更频繁地更改。例如,如果您想在测试数据库中测试代码。

在面向对象编程的世界中,DI与此类似。那里的值而不是常量文字是整个对象-但是将创建它们的代码从类代码中移出的原因是相似的-对象的更改比使用它们的代码更频繁。一个重要的情况是需要进行这样的改变,那就是测试。

这是我见过的关于依赖注入和依赖注入容器的最简单的解释:

无依赖注入

应用程序需要Foo(例如控制器),因此:应用程序创建Foo应用程序调用FooFoo需要Bar(例如服务),因此:Foo创建BarFoo调用Bar酒吧需要Bim(服务、存储库,…),因此:条形图创建Bim酒吧有点事

使用依赖注入

应用程序需要Foo,需要Bar,需要Bim,因此:应用程序创建Bim应用程序创建Bar并赋予它Bim应用程序创建Foo并给它Bar应用程序调用FooFoo调用Bar酒吧有点事

使用依赖注入容器

应用程序需要Foo,因此:应用程序从容器中获取Foo,因此:容器创建Bim容器创建Bar并赋予它Bim容器创建Foo并给它Bar应用程序调用FooFoo调用Bar酒吧有点事

依赖注入和依赖注入容器是不同的:

依赖注入是一种编写更好代码的方法DI容器是帮助注入依赖项的工具

您不需要容器来执行依赖注入。然而,容器可以帮助您。

摘自《扎实的Java开发人员:Java 7和多语言编程的关键技术》一书

DI是IoC的一种特殊形式,因此查找依赖项的过程是不受当前执行代码的直接控制。

依赖注入是解决“依赖混淆”需求的一种可能方案。依赖性混淆是一种将“明显”性质从向需要依赖性的类提供依赖性的过程中去除的方法,因此在某种程度上混淆了向所述类提供所述依赖性。这不一定是坏事。事实上,通过混淆向类提供依赖项的方式,类外部的某个东西负责创建依赖项,这意味着在各种情况下,可以向类提供不同的依赖项实现,而不需要对类进行任何更改。这对于在生产和测试模式之间切换非常有用(例如,使用“模拟”服务依赖)。

不幸的是,糟糕的部分是,有些人认为你需要一个专门的框架来进行依赖性混淆,如果你选择不使用特定的框架来做,那么你在某种程度上就是一个“低级”程序员。另一个非常令人不安的神话是,依赖性注入是实现依赖性混淆的唯一方法。这显然是历史性的,显然是100%错误的,但你很难说服一些人,依赖项注入可以替代依赖项混淆需求。

多年来,程序员们已经了解了依赖性混淆的需求,在考虑依赖性注入之前和之后,许多替代解决方案都已经发展起来。有工厂模式,但也有许多使用ThreadLocal的选项,其中不需要对特定实例进行注入-依赖关系被有效地注入到线程中,这样做的好处是使对象(通过方便的静态getter方法)可用于任何需要它的类,而无需向需要它的类别添加注释并设置复杂的XML“粘合”以实现这一点。当持久性需要依赖项(JPA/JDO或其他)时,它允许您更容易地实现“跨持久性”,并且域模型和业务模型类完全由POJO组成(即没有特定于框架的/锁定在注释中的)。