已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
当前回答
流行的答案毫无用处,因为它们以一种无用的方式定义依赖注入。让我们同意,“依赖性”是指我们的对象X所需要的一些预先存在的其他对象。但当我们说
$foo = Foo->new($bar);
我们只调用将参数传递到构造函数中。自从构造器被发明以来,我们一直在定期这样做。
“依赖注入”被认为是“控制反转”的一种类型,这意味着某些逻辑被从调用者中取出。当调用者传入参数时,情况并非如此,因此如果是DI,DI就不会意味着控制反转。
DI意味着在调用者和构造函数之间有一个中间层来管理依赖关系。Makefile是依赖注入的一个简单示例。“调用者”是在命令行上键入“make bar”的人,“构造函数”是编译器。Makefile指定bar依赖于foo,并执行
gcc -c foo.cpp; gcc -c bar.cpp
在执行
gcc foo.o bar.o -o bar
键入“makebar”的人不需要知道bar依赖于foo。在“makebar”和gcc之间注入了依赖关系。
中间层的主要目的不仅仅是将依赖项传递给构造函数,而是在一个地方列出所有依赖项,并向编码器隐藏它们(而不是让编码器提供它们)。
通常,中间层为构造的对象提供工厂,这些对象必须提供每个请求的对象类型都必须满足的角色。这是因为通过拥有一个隐藏构建细节的中间层,您已经受到了工厂施加的抽象惩罚,所以您不妨使用工厂。
其他回答
依赖注入是将依赖传递给其他对象或框架(依赖注入器)。
依赖注入使测试更容易。注入可以通过构造函数完成。
SomeClass()的构造函数如下:
public SomeClass() {
myObject = Factory.getObject();
}
问题:如果myObject涉及诸如磁盘访问或网络访问之类的复杂任务,则很难在SomeClass()上进行单元测试。程序员必须模拟myObject,并可能拦截工厂调用。
替代解决方案:
将myObject作为参数传入构造函数
public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
myObject可以直接传递,这使得测试更容易。
一种常见的替代方法是定义一个不做任何事情的构造函数。依赖注入可以通过setter完成。(h/t@MikeVella)。Martin Fowler记录了第三种选择(h/t@MarcDix),其中类显式地实现了程序员希望注入的依赖项的接口。
在没有依赖注入的情况下,很难在单元测试中隔离组件。
2013年,当我写下这个答案时,这是谷歌测试博客的一个主要主题。这对我来说仍然是最大的优势,因为程序员在运行时设计中并不总是需要额外的灵活性(例如,服务定位器或类似模式)。程序员通常需要在测试期间隔离类。
我将提出一个稍微不同的、简短而精确的依赖注入定义,侧重于主要目标,而不是技术手段(从这里开始):
依赖注入是创建静态、无状态的服务对象图,其中每个服务由其依赖关系。
我们在应用程序中创建的对象(无论我们是否使用Java、C#或其他面向对象语言)通常分为两类:无状态、静态和全局“服务对象”(模块),以及有状态、动态和本地“数据对象”。
模块图(服务对象图)通常在应用程序启动时创建。这可以使用容器(如Spring)完成,但也可以通过向对象构造函数传递参数来手动完成。这两种方法都有其优点和缺点,但在应用程序中使用DI肯定不需要框架。
一个要求是服务必须通过其依赖性进行参数化。这意味着什么完全取决于给定系统中采用的语言和方法。通常,这采用构造函数参数的形式,但使用setter也是一种选择。这也意味着(在调用服务方法时)对服务的用户隐藏服务的依赖关系。
何时使用?我会说,每当应用程序足够大时,将逻辑封装到单独的模块中,在模块之间使用依赖关系图,可以提高代码的可读性和可探索性。
依赖注入意味着一种方式(实际上是任何方式),代码的一部分(例如类)可以以模块化的方式访问依赖项(代码的其他部分,例如它所依赖的其他类),而不必对它们进行硬编码(因此它们可以自由更改或重写,甚至可以根据需要在其他时间加载)
(顺便说一句,是的,对于一个相当简单的概念,它已经成为一个过度炒作的25美元的名字),我的25美分
让我们想象一下,你想去钓鱼:
没有依赖注入,你需要自己处理所有的事情。你需要找一艘船,买一根鱼竿,寻找诱饵等等。当然,这是可能的,但这给你带来了很多责任。在软件方面,这意味着你必须对所有这些东西进行查找。通过依赖注入,其他人负责所有准备工作,并为您提供所需的设备。你将收到(“被注射”)船、鱼竿和鱼饵——所有这些都准备好使用。
我想既然每个人都为DI写过文章,让我问几个问题。。
当您有一个DI配置,其中所有实际实现(而不是接口)都将被注入到一个类中(例如控制器的服务),为什么这不是某种硬编码?如果我想在运行时更改对象怎么办?例如,我的配置已经表明,当我实例化MyController时,为FileLogger注入ILogger。但我可能想注入DatabaseLogger。每次我想更改AClass需要的对象时,我都需要查看两个地方——类本身和配置文件。这是如何让生活更轻松的?如果没有注入Aproperty的AClass,是否更难模拟?回到第一个问题。如果使用new object()不好,我们为什么要注入实现而不是接口?我想你们很多人都在说我们实际上是在注入接口,但配置要求您指定接口的实现。。不在运行时。。它在编译时是硬编码的。
这是基于@Adam N发布的答案。
为什么PersonService不再需要担心GroupMembershipService?您刚才提到了GroupMembership依赖于多个东西(对象/财产)。如果PService中需要GMService,您应该将其作为属性。不管你是否注射,你都可以模仿它。我唯一希望它被注入的时候是GMService是否有更具体的子类,这在运行时之前你不会知道。然后您需要注入子类。或者如果您想将其用作单例或原型。老实说,配置文件中的所有内容都是硬编码的,包括它将在编译时为类型(接口)注入的子类。
编辑
Jose Maria Arranz对DI的美好评论
DI通过消除确定依赖方向和编写任何粘合代码的任何需要来提高内聚性。
错误。依赖关系的方向是XML形式或注释,依赖关系是以XML代码和注释的形式编写的。XML和注释是源代码。
DI通过使您的所有组件模块化(即可更换)并具有相互定义良好的接口来减少耦合。
错误。您不需要DI框架来构建基于接口的模块化代码。
关于可替换:使用非常简单的.properties存档和Class.forName,您可以定义哪些类可以更改。如果代码的任何类都可以更改,Java不适合您,请使用脚本语言。顺便说一句:不重新编译就不能更改注释。
在我看来,DI框架只有一个原因:锅炉板减少。有了一个做得好的工厂系统,您可以像首选的DI框架一样做得更好、更可控、更可预测,DI框架可以减少代码(XML和注释也是源代码)。问题是这种锅炉板减少在非常简单的情况下是真实的(每个类一个实例以及类似的情况),有时在现实世界中,选择适当的服务对象不像将类映射到单个对象那样容易。