为什么要使用dependency injection?

我试图理解dependency injection (DI),并再次失败。 这似乎很愚蠢。 我的代码从来不是一团糟。 我几乎不写虚拟函数和接口(虽然我只做了一次蓝月亮),所有的configuration都被神奇地用json.net(有时使用XML序列化器)序列化成一个类。

我不太明白它解决了什么问题。 它看起来像一个方式来说:“嗨,当你遇到这个函数,返回一个这种types的对象,并使用这些参数/数据。
但是…为什么我会用这个? 注意我从来不需要使用object ,但是我明白这是什么意思。

在使用DI的网站或桌面应用程序中,有哪些实际情况? 我可以很容易地想出为什么有人可能想要在游戏中使用接口/虚函数的情况,但是在非游戏代码中使用非常罕见(难以记忆单个实例)。

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

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

(实际上,也有名词的界面,但我想在这里概括一下。)

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

为了得到这个观点:通常你的课程是相互依赖的。 例如,你可以有一个访问数据库的Database类(hah,surprise!;-)),但是你也希望这个类做访问数据库的日志logging。 假设你有另一个类Logger ,那么DatabaseLogger有一个依赖。

到现在为止还挺好。

您可以使用以下行在您的Database类中对此依赖关系进行build模:

 var logger = new Logger(); 

一切都很好 直到你意识到你需要一堆logging器的时候,这是没问题的:有时你想login到控制台,有时到文件系统,有时使用TCP / IP和远程日志logging服务器,等等。

当然,你不想改变你所有的代码(同时你有它的代码)并且replace所有的代码

 var logger = new Logger(); 

通过:

 var logger = new TcpLogger(); 

首先,这是没有趣味的。 其次,这是容易出错的。 第三,对于训练有素的猴子来说,这是愚蠢的,重复的工作。 所以你会怎么做?

显然,引入一个由所有各种logging器实现的接口ICanLog (或类似的)是个不错的主意。 所以你的代码中的第1步是你:

 ICanLog logger = new Logger(); 

现在types推断不再改变types,你总是有一个单一的界面来发展。 下一步是你不想一次又一次地拥有new Logger() 。 所以,你把可靠性创build到一个单一的中央工厂类,你得到的代码如下:

 ICanLog logger = LoggerFactory.Create(); 

工厂本身决定创build什么样的logging器。 您的代码不再在意,如果您想更改正在使用的logging器的types,请将其更改一次 :在工厂内部。

现在,当然,你可以概括这个工厂,并使其适用于任何types:

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

在某处,这个TypeFactory需要configuration数据,当请求一个特定的接口types时,实际的类将被实例化,所以你需要一个映射。 当然,你可以在代码中做这个映射,但是types改变意味着重新编译。 但是你也可以把这个映射放在一个XML文件中,例如。 这使您可以在编译时(!)之后更改实际使用的类,这意味着可以dynamic地重新编译!

给你一个有用的例子:想想一个没有正常logging的软件,但是当你的客户打电话问他求助,因为他有问题,你发给他的是一个更新的XMLconfiguration文件,现在他已经日志启用,您的支持可以使用日志文件来帮助您的客户。

现在,当你replace一个名字的时候,你最终得到了一个服务定位器的简单实现,它是控制反转的两种模式之一 (因为你反过来控制谁来决定要实例化的确切的类)。

总而言之,这会减less代码中的依赖关系,但是现在,所有的代码都依赖于中央的单一服务定位器。

dependency injection现在是这一行的下一步:只要摆脱这个单一的依赖到服务定位器:而不是各种各样的类请求服务定位器的具体接口的实现,你再次 – 控制谁实例化什么。

使用dependency injection,你的Database类现在有一个构造函数,需要一个ICanLogtypes的参数:

 public Database(ICanLog logger) { ... } 

现在你的数据库总是有一个logging器来使用,但是它不知道这个logging器来自哪里。

而这正是DI框架发挥作用的地方:您再次configuration您的映射,然后请您的DI框架为您实例化您的应用程序。 由于Application类需要一个ICanPersistData实现,所以注入了一个Database实例 – 但是为此,它必须首先创build一个为ICanLogconfiguration的logging器实例。 等等 …

所以,简而言之就是:dependency injection是如何去除代码中依赖关系的两种方法之一。 这对于编译后的configuration更改非常有用,对于unit testing来说是非常有用的(因为它使注入stub和/或mock变得非常简单)。

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

但基本上就是这样。

希望有所帮助。

PS:我在这里描述的是一种叫做构造函数注入的技术,还有属性注入 ,其中不是构造函数参数,而是用于定义和解决依赖关系的属性。 将属性注入视为可选的依赖项,将构造函数注入视为必需的依赖项。 但是这个问题的讨论超出了这个问题的范围。

我想很多时候人们会对dependency injection和dependency injection框架 (或者称为容器 )之间的区别感到困惑。

dependency injection是一个非常简单的概念。 而不是这个代码:

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

就是这样。 认真。 这给了你很多好处。 两个重要的function是从中心位置( Main()函数)控制function,而不是将其分散到整个程序中,并且更容易地单独testing每个类(因为您可以将模拟对象或其他伪造的对象传入它的构造函数而不是真正的价值)。

当然,缺点是你现在有一个知道你的程序使用的所有类的超级函数。 这是DI框架可以帮助的。 但是如果你不明白为什么这个方法是有价值的,我build议首先从手动dependency injection开始,这样你可以更好地理解那里的各种框架可以为你做什么。

正如其他答案所述,dependency injection是一种在使用它的类之外创build依赖的方法。 你从外面注入他们,并把他们的创作从class级里面拿走。 这也是为什么dependency injection是控制反转 (IoC)原理的一个实现。

IoC是原则,其中DI是模式。 根据我的经验,你可能“需要多个logging器”的原因实际上从来没有遇到过,但实际的原因是,当你testing某些东西时,你确实需要它。 一个例子:

我的特色:

当我看到一个提议,我想标记,我自动看着它,所以我不会忘记这样做。

你可能会这样testing:

 [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某个地方,它会为您创build一个对象,如下所示:

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

这里的问题是,这个testing很可能总是失败,因为被设置的date与被声明的date不同,即使你只是把DateTime.Now放在testing代码中,它可能会被closures几毫秒并因此总是失败。 现在一个更好的解决scheme将是为此创build一个接口,它允许您控制将设置的时间:

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

接口是抽象的。 一个是真实的东西,另一个允许你假冒一些需要的地方。 testing可以像这样改变:

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

像这样,你通过注入依赖(获取当前时间)来应用“控制反转”原理。 这样做的主要原因是为了更简单的unit testing,还有其他的方法。 例如,一个接口和一个类在这里是不必要的,因为在C#中函数可以作为variables传递,所以可以使用Func<DateTime>来实现相同的接口。 或者,如果采取dynamic方法,只需传递任何具有等效方法的对象( 鸭子键入 ),并且根本不需要接口。

你几乎不需要多于一个logging器。 尽pipe如此,dependency injection对于静态types代码(例如Java或C#)是必不可less的。

而且……还应该注意的是,一个对象只能在运行时正确地实现它的目的,如果它的所有依赖关系都可用,那么在build立属性注入方面没有太多的用处。 在我看来,当构造函数被调用时,所有的依赖关系都应该被满足,所以构造函数注入是一个需要解决的问题。

我希望有所帮助。

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

例如,我们是一家中央支付提供商,与世界各地的许多支付提供商合作。 但是,当提出请求时,我不知道要拨打哪个付款处理器。 我可以用一大堆的开关盒来编程,例如:

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

现在想象一下,现在您需要将所有这些代码保存在一个类中,因为它没有正确分离,您可以想象,对于您将支持的每个新处理器,您需要创build一个新的if // switch每一种方法,只是通过使用dependency injection(或者控制反转 – 因为它有时被称为,意味着控制程序运行的人只有在运行时才知道,而不是复杂的),这只会变得更加复杂。非常整洁和可维护。

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

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

使用DI的主要原因是你要把知识的实施知识的责任放在那里。 DI的思想与接口封装和devise非常接近。 如果前端从后端询问一些数据,那么前端如何解决这个问题对于前端来说是不重要的。 这取决于请求处理者。

这在OOP中已经很常见了。 多次创build代码片段,如:

 I_Dosomething x = new Impl_Dosomething(); 

缺点是实现类仍然是硬编码的,因此具有实现被使用的知识的前端。 DI进一步采用接口devise,前端唯一需要了解的是接口知识。 在DYI和DI之间是服务定位器的模式,因为前端必须提供一个键(存在于服务定位器的registry中)以使其请求得到解决。 服务定位器示例:

 I_Dosomething x = ServiceLocator.returnDoing(String pKey); 

DI例子:

 I_Dosomething x = DIContainer.returnThat(); 

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

D(b)I:dependency injection和接口devise。 但这个限制并不是一个很大的实际问题。 使用D(b)I的好处是它可以为客户和提供者之间的沟通提供服务。 接口是对象或一组行为的透视图。 后者在这里至关重要。

我更喜欢与D(b)I一起编写服务合同的pipe理。 他们应该一起去。 在我看来,使用D(b)I作为技术解决scheme而没有对服务合同进行组织pipe理,因为DI只是一个额外的封装层,所以不是很有利。 但是,如果您可以将其与组织pipe理一起使用,则可以真正利用我提供的组织原则D(b)。 从长远来看,它可以帮助您与客户和其他技术部门就testing,版本和替代scheme的开发等主题进行沟通。 当你在硬编码类中有一个隐式接口的时候,那么当你使用D(b)I进行显式定义的时候,它是否会随着时间的推移而less得多地传播。 这一切都归结为维修,这是随着时间的推移,而不是一次。 🙂