为什么要使用依赖注入?

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

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

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

269213 次浏览

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

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

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

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

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

到目前为止,一切顺利。

你可以在你的Database类中建模这个依赖:

var logger = new Logger();

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

当然,您希望更改所有代码(同时您有大量代码)并替换所有行

var logger = new Logger();

由:

var logger = new TcpLogger();

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

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

ICanLog logger = new Logger();

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

ICanLog logger = LoggerFactory.Create();

工厂自己决定创建哪种记录器。您的代码不再关心这个问题,如果您想要更改所使用的记录器的类型,可以将其更改为一次: Inside the factory。

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

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

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

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

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

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

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

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

public Database(ICanLog logger) { ... }

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

这就是DI框架发挥作用的地方:您再次配置映射,然后要求DI框架为您实例化应用程序。由于Application类需要ICanPersistData实现,因此注入了Database的实例——但为此它必须首先创建为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();
}

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

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

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

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

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

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI的例子:

I_Dosomething x = DIContainer.returnThat();

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

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

我更喜欢在编码方面与D(b)I一起管理服务合同。他们应该一起去。在我看来,使用D(b)I作为技术解决方案而不使用服务契约的组织管理并不是非常有益的,因为DI只是一个额外的封装层。但是当你将它与组织管理结合使用时,你就可以真正利用D(b)I提供的组织原则。 从长远来看,它可以帮助您构建与客户端和其他技术部门在测试、版本控制和替代方案开发等主题上的沟通。当你在硬编码类中使用隐式接口时,随着时间的推移,它的可交流性会比使用D(b)I显式接口时差得多。这一切都归结为维护,这是随着时间的推移而不是一次。: -) < / p >

正如其他答案所述,依赖注入是在使用它的类之外创建依赖项的一种方式。您从外部注入它们,并从类内部控制它们的创建。这也是依赖注入实现控制反转 (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.Now,它也可能相差几毫秒,因此总是失败。现在一个更好的解决方案是创建一个接口,允许你控制将设置的时间:

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

就像这样,你应用了“控制反转”;原理,通过注入依赖项(获取当前时间)。这样做的主要原因是为了更容易进行隔离单元测试,还有其他方法。例如,这里的接口和类是不必要的,因为在c#中,函数可以作为变量传递,所以可以使用Func<DateTime>代替接口来实现相同的功能。或者,如果您采用动态方法,您只需传递具有等效方法的任何对象(duck typing),并且您根本不需要接口。

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

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

我认为经典的答案是创建一个更加解耦的应用程序,它不知道在运行时将使用哪个实现。

例如,我们是一家中央支付提供商,与世界各地的许多支付提供商合作。但是,当发出请求时,我不知道要调用哪个支付处理器。我可以用大量的开关情况编写一个类,比如:

class PaymentProcessor{


private String type;


public PaymentProcessor(String type){
this.type = type;
}


public void authorize(){
if (type.equals(Consts.PAYPAL)){
// Do this;
}
else if(type.equals(Consts.OTHER_PROCESSOR)){
// Do that;
}
}
}

现在想象一下,现在你需要在一个类中维护所有这些代码,因为它没有正确地解耦,你可以想象一下,对于你支持的每一个新处理器,你都需要为每个方法创建一个新的if // switch case,这只会变得更复杂,然而,通过使用依赖注入(或控制反转-有时它被称为,这意味着谁控制程序的运行只在运行时知道,而不是复杂),您可以实现一些非常整洁和可维护的东西。

class PaypalProcessor implements PaymentProcessor{


public void authorize(){
// Do PayPal authorization
}
}


class OtherProcessor implements PaymentProcessor{


public void authorize(){
// Do other processor authorization
}
}


class PaymentFactory{


public static PaymentProcessor create(String type){


switch(type){
case Consts.PAYPAL;
return new PaypalProcessor();


case Consts.OTHER_PROCESSOR;
return new OtherProcessor();
}
}
}


interface PaymentProcessor{
void authorize();
}

**代码不会编译,我知道:)

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

# EYZ0

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

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

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