我试图理解依赖注入(DI),但又一次失败了。这看起来很傻。我的代码从来不是一团糟;我几乎不编写虚函数和接口(尽管我很少写一次),而且我的所有配置都神奇地使用json.net序列化成一个类(有时使用XML序列化器)。

我不太明白它能解决什么问题。它看起来像是在说:“嗨。当你遇到这个函数时,返回一个该类型的对象,并使用这些参数/数据。” 但是…我为什么要用这个?注意,我从来没有需要使用object,但我知道这是为了什么。

在构建网站或桌面应用程序时,使用DI的真实情况是什么?我可以很容易地想出为什么有人想要在游戏中使用界面/虚拟函数的例子,但在非游戏代码中使用这种方法的情况非常罕见(我想不起一个例子)。


当前回答

坦率地说,我相信人们使用这些依赖注入库/框架是因为他们只知道如何在运行时做事情,而不是在加载时。所有这些疯狂的机制都可以通过设置CLASSPATH环境变量(或其他语言等效变量,如PYTHONPATH、LD_LIBRARY_PATH)来替代,以指向特定类的替代实现(都具有相同的名称)。所以在接受的答案中,你只需要留下你的代码

var logger = new logger() //简单的代码

适当的记录器将被实例化,因为JVM(或其他运行时或。so加载器)将从通过上面提到的环境变量配置的类中获取它。

不需要把所有东西都变成一个接口,不需要疯狂地生成破碎的对象,把东西注入其中,不需要疯狂地构造函数,把内部机制的每一块都暴露给世界。只要使用你所使用的任何语言的本地功能,而不是想出在任何其他项目中都不起作用的方言。

注:这同样适用于测试/模拟。您可以很好地设置您的环境,在加载时加载适当的模拟类,而跳过模拟框架的疯狂。

其他回答

首先,我想解释一下我为这个答案所做的一个假设。这并不总是正确的,但经常是这样的:

接口是形容词;类是名词。

(实际上,有些接口也是名词,但我想在这里概括一下。)

例如,一个接口可能是IDisposable, IEnumerable或IPrintable。类是这些接口中的一个或多个的实际实现:List或Map都可以是IEnumerable的实现。

要点在于:通常你的类是相互依赖的。例如,你可以有一个数据库类来访问你的数据库(哈,惊喜!;-)),但是您还希望这个类执行关于访问数据库的日志记录。假设您有另一个类Logger,那么Database就依赖于Logger。

到目前为止,一切顺利。

你可以在你的数据库类中建模这个依赖:

var logger = new Logger();

一切都很好。直到有一天您意识到您需要一堆日志记录程序:有时您想要记录到控制台,有时想要记录到文件系统,有时使用TCP/IP和远程日志服务器,等等……

当然,你不希望改变你所有的代码(同时你有无数的代码)并替换所有的行

var logger = new Logger();

by:

var logger = new TcpLogger();

首先,这一点都不好玩。其次,这很容易出错。第三,这对一只训练有素的猴子来说是愚蠢而重复的工作。你会怎么做呢?

显然,引入一个接口ICanLog(或类似的)是一个很好的主意,可以由各种各样的日志记录器实现。代码中的第一步是:

ICanLog logger = new Logger();

现在类型推断不再改变类型,您总是有一个单独的接口来开发。下一步是不希望反复使用新的Logger()。所以你把创建新实例的可靠性放在一个单一的中央工厂类中,你会得到这样的代码:

ICanLog logger = LoggerFactory.Create();

工厂自己决定创建哪种记录器。您的代码不再关心这个问题,如果您想要更改所使用的记录器的类型,只需更改一次:在工厂内部。

当然,现在你可以泛化这个工厂,让它适用于任何类型:

ICanLog logger = TypeFactory.Create<ICanLog>();

当请求特定接口类型时,这个TypeFactory需要实例化实际类的配置数据,因此需要映射。当然,您可以在代码中进行这种映射,但是类型更改意味着重新编译。但是你也可以把这个映射放在一个XML文件中,例如。这允许您在编译之后更改实际使用的类(!),这意味着动态地,而不需要重新编译!

举一个有用的例子:假设一个软件不能正常记录日志,但是当客户因为遇到问题而打电话并寻求帮助时,您发送给他的只是一个更新的XML配置文件,现在他启用了日志记录,您的支持可以使用日志文件来帮助客户。

现在,稍微替换一下名称,就得到了Service Locator的简单实现,它是控制反转的两种模式之一(因为您反转了谁决定具体实例化什么类的控制)。

总之,这减少了代码中的依赖关系,但现在所有的代码都依赖于中心的单一服务定位器。

依赖注入现在是这一行的下一步:只需摆脱对服务定位器的单个依赖:而不是各种类向服务定位器请求特定接口的实现,您—再一次—恢复了谁实例化什么的控制。

使用依赖注入,你的数据库类现在有一个构造函数,它需要一个类型为ICanLog的参数:

public Database(ICanLog logger) { ... }

现在您的数据库总是有一个记录器可以使用,但是它不知道这个记录器来自哪里。

这就是DI框架发挥作用的地方:您再次配置映射,然后要求DI框架为您实例化应用程序。由于Application类需要一个ICanPersistData实现,因此注入了一个数据库实例——但为此它必须首先创建一个为ICanLog配置的记录器类型的实例。等等……

因此,长话短说:依赖注入是在代码中删除依赖的两种方法之一。它对于编译后的配置更改非常有用,对于单元测试也非常有用(因为它使注入存根和/或模拟变得非常容易)。

在实践中,如果没有服务定位器,有些事情是不能做的(例如,如果您事先不知道特定接口需要多少实例:DI框架总是每个参数只注入一个实例,但当然,您可以在循环中调用服务定位器),因此大多数情况下,每个DI框架也提供一个服务定位器。

但基本上,就是这样。

附注:我在这里描述的是一种称为构造函数注入的技术,还有属性注入,其中没有构造函数参数,但属性被用于定义和解析依赖关系。可以将属性注入视为可选依赖项,将构造函数注入视为强制依赖项。但是关于这个问题的讨论超出了这个问题的范围。

我认为很多时候人们会混淆依赖注入和依赖注入框架(或者通常被称为容器)之间的区别。

依赖注入是一个非常简单的概念。而不是下面的代码:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

你可以这样写代码:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

就是这样。认真对待。这给了你很多好处。两个重要的特性是能够从一个中心位置(Main()函数)控制功能,而不是将其分散到整个程序中,以及能够更容易地单独测试每个类(因为可以将模拟对象或其他伪造对象传递到其构造函数中,而不是传递实际值)。

当然,缺点是现在只有一个超级函数,它知道程序使用的所有类。这就是依赖注入框架可以提供的帮助。但是,如果您无法理解为什么这种方法有价值,我建议您先从手动依赖注入开始,这样您就可以更好地了解各种框架可以为您做些什么。

坦率地说,我相信人们使用这些依赖注入库/框架是因为他们只知道如何在运行时做事情,而不是在加载时。所有这些疯狂的机制都可以通过设置CLASSPATH环境变量(或其他语言等效变量,如PYTHONPATH、LD_LIBRARY_PATH)来替代,以指向特定类的替代实现(都具有相同的名称)。所以在接受的答案中,你只需要留下你的代码

var logger = new logger() //简单的代码

适当的记录器将被实例化,因为JVM(或其他运行时或。so加载器)将从通过上面提到的环境变量配置的类中获取它。

不需要把所有东西都变成一个接口,不需要疯狂地生成破碎的对象,把东西注入其中,不需要疯狂地构造函数,把内部机制的每一块都暴露给世界。只要使用你所使用的任何语言的本地功能,而不是想出在任何其他项目中都不起作用的方言。

注:这同样适用于测试/模拟。您可以很好地设置您的环境,在加载时加载适当的模拟类,而跳过模拟框架的疯狂。

使用DI的主要原因是,您希望将实现知识的责任放在知识所在的位置。依赖注入的思想非常符合封装和按接口设计。 如果前端向后端请求一些数据,那么后端如何解决这个问题对于前端来说并不重要。这取决于请求处理程序。

这在OOP中已经很常见了。很多时候创建如下代码段:

I_Dosomething x = new Impl_Dosomething();

缺点是实现类仍然是硬编码的,因此前端有使用哪个实现的知识。DI将接口设计进一步推进了一步,前端需要知道的唯一一件事就是接口的知识。 在DYI和DI之间是服务定位器的模式,因为前端必须提供一个键(出现在服务定位器的注册表中)来解析它的请求。 服务定位器示例:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

在操作:

I_Dosomething x = DIContainer.returnThat();

DI的要求之一是容器必须能够找出哪个类是哪个接口的实现。因此,DI容器需要强类型设计,并且每个接口同时只能有一个实现。如果同时需要一个接口的多个实现(如计算器),则需要服务定位器或工厂设计模式。

D(b)I:依赖注入和接口设计。 不过,这个限制并不是一个非常大的实际问题。使用D(b)I的好处是它为客户端和提供者之间的通信提供服务。接口是一个对象或一组行为的视角。后者在这里至关重要。

I prefer the administration of service contracts together with D(b)I in coding. They should go together. The use of D(b)I as a technical solution without organizational administration of service contracts is not very beneficial in my point of view, because DI is then just an extra layer of encapsulation. But when you can use it together with organizational administration you can really make use of the organizing principle D(b)I offers. It can help you in the long run to structure communication with the client and other technical departments in topics as testing, versioning and the development of alternatives. When you have an implicit interface as in a hardcoded class, then is it much less communicable over time then when you make it explicit using D(b)I. It all boils down to maintenance, which is over time and not at a time. :-)

正如其他答案所述,依赖注入是在使用它的类之外创建依赖项的一种方式。您从外部注入它们,并从类内部控制它们的创建。这也是为什么依赖注入是控制反转(IoC)原则的实现。

IoC是原则,DI是模式。根据我的经验,您可能“需要多个记录器”的原因实际上从未得到满足,但实际的原因是,无论何时测试某个东西,您都确实需要它。一个例子:

我的特点:

当我看报价时,我想要标记我是自动看的,这样我就不会忘记这么做。

你可以这样测试:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

在OfferWeasel的某处,它像这样为你创建了一个offer对象:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

这里的问题是,这个测试很可能总是失败,因为所设置的日期将不同于所断言的日期,即使您只输入DateTime。现在在测试代码中,它可能会关闭几毫秒,因此总是会失败。现在一个更好的解决方案是创建一个接口,允许你控制将设置的时间:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

接口是抽象。一个是真实的东西,另一个可以让你在需要的时候假装。测试可以这样更改:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Like this, you applied the "inversion of control" principle, by injecting a dependency (getting the current time). The main reason to do this is for easier isolated unit testing, there are other ways of doing it. For example, an interface and a class here is unnecessary since in C# functions can be passed around as variables, so instead of an interface you could use a Func<DateTime> to achieve the same. Or, if you take a dynamic approach, you just pass any object that has the equivalent method (duck typing), and you don't need an interface at all.

您几乎不需要超过一个记录器。尽管如此,依赖注入对于静态类型的代码(如Java或c#)是必不可少的。

和… 还应该注意的是,如果对象的所有依赖项都可用,则对象只能在运行时正确地实现其目的,因此设置属性注入没有太大用处。在我看来,当调用构造函数时,所有的依赖关系都应该得到满足,所以构造函数注入是可以使用的。