依赖项注入容器的好处是什么?
我了解依赖注入本身的好处。让我们以Spring为例。我还了解其他Spring功能(如AOP,各种帮助程序等)的好处。我只是想知道XML配置的好处是什么,例如:
<bean id="Mary" class="foo.bar.Female"> <property name="age" value="23"/> </bean> <bean id="John" class="foo.bar.Male"> <property name="girlfriend" ref="Mary"/> </bean>
与普通的旧Java代码相比,例如:
Female mary = new Female(); mary.setAge(23); Male john = new Male(); john.setGirlfriend(mary);
它更容易调试,检查编译时间,并且只懂Java的人都可以理解。
那么,依赖注入框架的主要目的是什么? (或者一段显示其好处的代码。)
更新:
的情况下
IService myService;// ... public void doSomething() { myService.fetchData(); }
如果有多个,IoC框架如何猜测要注入的myService的哪种实现?如果给定接口只有一个实现,而我让IoC容器自动决定使用它,则在出现第二个实现后,它将被破坏。而且,如果有意仅使用一种可能的接口实现,则无需注入该接口。
看到IoC的一小部分配置显示出它的好处,这将真的很有趣。我已经使用Spring一段时间了,我无法提供这样的示例。我可以显示单行代码,这些代码说明了休眠,dwr和我使用的其他框架的好处。
更新2:
我意识到,无需重新编译即可更改IoC配置。真的是个好主意吗?我可以理解,当有人想更改数据库凭据而不重新编译时,他可能不是开发人员。在实践中,除开发人员外,其他人多久更改一次IoC配置?我认为对于开发人员而言,无需重新编译该特定类,而无需更改配置。对于非开发人员,我们可能希望使他的生活更轻松,并提供一些更简单的配置文件。
更新3:
External configuration of mapping between interfaces and their concrete implementations
使它具有延展性有什么好处?我们不必将所有代码都放在外部,尽管我们可以将其放到ClassName.java.txt文件中,并且可以即时读取和手动编译,但避免了重新编译。为什么要避免编译?
You save coding time because you provide mappings declaratively, not in a procedural code
我知道有时声明式方法可以节省时间。例如,我仅声明一次bean属性与DB列之间的映射,并且hibernate在加载,保存,构建基于HSQL的SQL等方式时使用此映射。在Spring的情况下(在我的示例中),声明具有更多的行,并且与相应的代码具有相同的表现力。如果有这样的声明比代码短的示例,我希望看到它。
Inversion of Control principle allows for easy unit testing because you can replace real implementations with fake ones (like replacing SQL database with an in-memory one)
我确实了解控制好处的反转(我更喜欢将这里讨论的设计模式称为"依赖注入",因为IoC更普遍,有许多种控制方式,而我们只反转其中一种初始化控制)。我在问为什么有人需要编程语言以外的东西。我绝对可以使用代码将真实的实现替换为假的实现。并且此代码将表达与配置相同的内容,它将仅使用伪值初始化字段。
mary = new FakeFemale();
我确实了解DI的好处。与配置相同功能的代码相比,我不了解外部XML配置可以带来哪些好处。我不认为应该每天编译,但我还活着,所以应该避免编译。我认为DI的配置是声明式方法的不良示例。如果声明一次,并且以不同的方式(例如休眠cfg)多次使用,则声明很有用。在这种方式下,bean属性和DB列之间的映射用于保存,加载,构建搜索查询等。代码,就像在这个问题的开头一样,可以吗?而且它仅用于bean初始化,不是吗?这意味着声明性方法不会在此处添加任何内容,是吗?
当我声明休眠映射时,我只是给休眠提供了一些信息,并且它基于它来工作,所以我不告诉它怎么做。如果是spring,我的声明会告诉spring到底要这么做,为什么要声明它,为什么不这样做呢?
最后更新:
伙计们,很多答案告诉我有关依赖注入的知识,我知道这很好。
问题是关于DI配置的目的,而不是初始化代码,我倾向于认为初始化代码更短,更清晰。
到目前为止,我对问题的唯一答案是,当配置更改时,它避免了重新编译。我想我应该提出另一个问题,因为这对我来说是个大秘密,为什么在这种情况下应避免编译。
解决方案
在.NET世界中,大多数IoC框架都提供XML和代码配置。
例如,StructureMap和Ninject使用流畅的界面来配置容器。我们不再受限于使用XML配置文件。 Spring也存在于.NET中,由于它是他的主要历史配置界面,因此在很大程度上依赖XML文件,但是仍然可以以编程方式配置容器。
我们无需在每次更改配置时都重新编译代码。它将简化程序的部署和维护。例如,我们只需在配置文件中进行一次更改即可将一个组件与另一个组件交换。
这是一个非常棘手的问题,但是我倾向于同意大量的xml配置并不能真正带来很多好处。我希望我的应用程序尽可能地减少依赖关系,包括庞大的框架。
它们很多时候简化了代码,但是它们也具有复杂性的开销,这使得跟踪问题变得相当困难(我亲眼目睹了此类问题,而直接使用Java会更容易处理)。
我想这取决于样式,以及我们是否满意……我们是否喜欢自己开发解决方案并从内到外了解它的好处,还是依赖现有的解决方案,而当配置不当时,这些解决方案可能会变得很困难。就对吗?这都是一个权衡。
但是,XML配置有点像是我的烦恼……我试图不惜一切代价避免使用它。
Spring也有一个属性加载器。我们使用这种方法来设置取决于环境的变量(例如,开发,测试,验收,生产等)。例如,这可能是要收听的队列。
如果没有理由更改属性,也没有理由以这种方式配置它。
情况非常简单,因此不需要像Spring这样的IoC(控制反转)容器。另一方面,当我们"对接口进行编程,而不是对实现进行编程"(在OOP中是一种很好的做法)时,我们可以拥有如下代码:
IService myService; // ... public void doSomething() { myService.fetchData(); }
(请注意,myService的类型是IService-一个接口,而不是具体的实现)。现在,当我们有许多接口和许多实现时,让IoC容器在初始化期间自动提供IService的正确具体实例可能很方便,手动进行操作可能很麻烦。 IoC容器(依赖注入框架)的主要优点是:
- 接口之间的映射及其具体实现的外部配置
- IoC容器处理一些棘手的问题,例如解决复杂的依赖关系图,管理组件的寿命等。
- 节省编码时间,因为我们以声明方式而不是在过程代码中提供映射
- "控制反转"原理使单元测试变得容易,因为我们可以用假的实例替换实际的实现(例如用内存中的实例替换SQL数据库)
我们可以为女友加入新的实现方式。因此,无需重新编译代码即可注入新的女性。
<bean id="jane" class="foo.bar.HotFemale"> <property name="age" value="19"/> </bean> <bean id="mary" class="foo.bar.Female"> <property name="age" value="23"/> </bean> <bean id="john" class="foo.bar.Male"> <property name="girlfriend" ref="jane"/> </bean>
(以上假设Female和HotFemale实现相同的GirlfFriend接口)
依赖注入是一种编码风格,其根源在于观察到对象委托通常是一种比对象继承更有用的设计模式(即对象具有-关系比对象具有-关系更有用)。为了使DI起作用,还需要另外一种成分,即创建对象接口。结合了这两种强大的设计模式,软件工程师迅速意识到,他们可以创建灵活的松耦合代码,因此依赖注入的概念诞生了。但是直到对象反射在某些高级语言中可用之后,DI才真正开始发展。反射组件是当今大多数DI系统的核心,因为DI的真正酷的方面要求能够以编程方式选择对象,并使用独立于对象本身的系统将其配置并注入到其他对象中。
语言必须对常规的面向对象的编程技术以及对象接口和对象反射(例如Java和C#)提供良好的支持。尽管可以在C ++系统中使用DI模式构建程序,但由于缺乏适当语言的反射支持,因此无法支持应用程序服务器和其他DI平台,因此限制了DI模式的表现力。
使用DI模式构建的系统的优点:
- DI代码更易于重用,因为将"依赖"功能外推到定义明确的接口中,从而允许将其配置由适当的应用程序平台处理的单独对象随意插入其他对象。
- DI代码更容易测试。通过构建实现应用程序逻辑所期望的接口的"模拟"对象,可以在黑盒中测试该对象表达的功能。
- DI代码更加灵活。它是与生俱来的松耦合代码-到了极致。这样,程序员就可以仅根据一端的所需接口和另一端的表示接口来选择和选择对象的连接方式。
- DI对象的外部(Xml)配置意味着其他人可以在无法预料的方向上自定义代码。
- 外部配置也是关注点模式的分离,因为对象初始化和对象相互依赖性管理的所有问题都可以由应用程序服务器处理。
- 请注意,使用DI模式不需要外部配置,对于简单的互连,一个小的构建器对象通常就足够了。两者之间要在灵活性上进行权衡。构建器对象的选项不如外部可见的配置文件灵活。 DI系统的开发人员必须权衡灵活性而不是便利性的优势,请注意配置文件中表示的对对象构造的小规模细粒度控制可能会增加混乱和维护成本。
绝对地,DI代码似乎更麻烦,使所有配置对象以注入其他对象的XML文件的缺点显得困难。但是,这就是DI系统的重点。通过将代码对象作为一系列配置设置进行混合和匹配的能力,我们可以使用第三方代码构建复杂的系统,而无需编写任何代码。
问题中提供的示例仅涉及适当分解的DI对象库可以提供的表达能力的表面。通过一些实践和大量自律,大多数DI专业人员发现他们可以构建具有100%测试应用程序代码覆盖率的系统。仅这一点是非同寻常的。这不是数百行代码的小型应用程序的100%测试覆盖率,而是包含数十万行代码的应用程序的100%测试覆盖率。我不敢描述任何其他提供这种可测试性的设计模式。
我们说对了,因为只有几十行代码的应用程序比几个对象以及一系列XML配置文件更容易理解。但是,与大多数强大的设计模式一样,在继续向系统添加新功能时会发现收益。
简而言之,基于DI的大规模应用程序更易于调试和理解。虽然Xml配置不是"编译时检查"的,但是如果作者尝试将具有不兼容接口的对象注入到另一个对象中,那么作者意识到的所有应用程序服务都将向开发人员提供错误消息。并且大多数提供覆盖所有已知对象配置的"检查"功能。通过检查要注入的对象A是否实现了对象B对于所有已配置的对象注入所需的接口,可以轻松而快速地完成此操作。
最吸引人的原因之一是"好莱坞原则":不要打电话给我们,我们会打电话给我们。不需要组件来查找其他组件和服务本身;相反,它们是自动提供给它的。在Java中,这意味着不再需要在组件内部进行JNDI查找。
孤立地对组件进行单元测试也容易得多:我们无需使用实际的实现,而是简单地使用(可能是自动生成的)模拟,而不是为其提供所需组件的实际实现。
易于将部分配置组合为最终的完整配置。
例如,在Web应用程序中,通常在单独的配置文件中指定模型,视图和控制器。使用声明性方法,我们可以加载,例如:
`
UI-context.xml Model-context.xml Controller-context.xml
`
或者加载不同的UI和一些额外的控制器:
`
AlternateUI-context.xml Model-context.xml Controller-context.xml ControllerAdditions-context.xml
`
要在代码中执行相同的操作,需要用于组合部分配置的基础结构。用代码做并非不可能,但是使用IoC框架当然更容易做。
使用DI容器的原因是我们不必在代码中预先配置十亿个属性,而这些属性只是简单的getter和setter。我们真的要用新的X()对所有代码进行硬编码吗?当然,我们可以使用默认值,但是DI容器允许创建单例,这非常简单,并且可以使我们专注于代码的细节,而不是初始化它的其他任务。
例如,Spring允许我们实现InitializingBean接口并添加afterPropertiesSet方法(也可以指定" init-method"以避免将代码耦合到Spring)。这些方法将允许我们确保在启动时正确配置了在类实例中指定为字段的任何接口,然后我们不再需要对getter和setter进行空检查(假设我们确实允许单例保持线程安全) )。
此外,用DI容器进行复杂的初始化要比自己进行复杂得多。例如,我协助使用XFire(不是CeltiXFire,我们仅使用Java 1.4)。该应用程序使用了Spring,但不幸的是它使用了XFire的services.xml配置机制。当元素集合需要声明其具有零个或者多个实例而不是一个或者多个实例时,我必须重写此特定服务提供的某些XFire代码。
在其Spring bean模式中定义了某些XFire默认值。因此,如果我们使用Spring来配置服务,则可以使用Bean。相反,发生的事情是我不得不在services.xml文件中提供特定类的实例,而不是使用bean。为此,我需要提供构造函数并设置XFire配置中声明的引用。我需要进行的真正更改要求我重载单个类。
但是,由于使用了services.xml文件,我不得不创建四个新类,根据其构造函数中Spring配置文件中的默认值设置它们的默认值。如果我们能够使用Spring配置,那么我可以这样说:
<bean id="base" parent="RootXFireBean"> <property name="secondProperty" ref="secondBean" /> </bean> <bean id="secondBean" parent="secondaryXFireBean"> <property name="firstProperty" ref="thirdBean" /> </bean> <bean id="thirdBean" parent="thirdXFireBean"> <property name="secondProperty" ref="myNewBean" /> </bean> <bean id="myNewBean" class="WowItsActuallyTheCodeThatChanged" />
相反,它看起来像这样:
public class TheFirstPointlessClass extends SomeXFireClass { public TheFirstPointlessClass() { setFirstProperty(new TheSecondPointlessClass()); setSecondProperty(new TheThingThatWasHereBefore()); } } public class TheSecondPointlessClass extends YetAnotherXFireClass { public TheSecondPointlessClass() { setFirstProperty(TheThirdPointlessClass()); } } public class TheThirdPointlessClass extends GeeAnotherXFireClass { public TheThirdPointlessClass() { setFirstProperty(new AnotherThingThatWasHereBefore()); setSecondProperty(new WowItsActuallyTheCodeThatChanged()); } } public class WowItsActuallyTheCodeThatChanged extends TheXFireClassIActuallyCareAbout { public WowItsActuallyTheCodeThatChanged() { } public overrideTheMethod(Object[] arguments) { //Do overridden stuff } }
因此,最终结果是必须将四个添加的,几乎没有意义的Java类添加到代码库中,以实现获得一个添加类和一些简单的依赖容器信息的效果。这不是"证明规则的例外",而是规则……当DI容器中已经提供了属性,而我们只是为了适应特殊情况而对其进行更改时,处理代码中的怪癖会更加简洁。这种情况经常发生。
就我自己而言,使用IoC(并使用外部配置)的主要原因之一是围绕以下两个方面:
- 测验
- 生产维护
测验
如果将测试分为3个场景(在大规模开发中这是很正常的):
- 单元测试
- 整合测试
- 黑匣子测试
我们要针对的是最后两个测试方案(集成和黑匣子),而不是重新编译应用程序的任何部分。
如果任何测试方案都要求我们更改配置(即:使用其他组件来模拟银行集成或者执行性能负载),则可以轻松地进行处理(这确实是在配置服务器的DI端的好处下)。 IoC虽然。
此外,如果应用程序在多个站点(具有不同的服务器和组件配置)上使用,或者在实时环境中具有更改的配置,则可以使用后期测试来验证该应用程序将处理这些更改。
生产
作为开发人员,我们没有(也不应该)控制生产环境(尤其是当应用分发到多个客户或者单独的站点时),这对我来说是同时使用IoC和外部配置的真正好处,因为需要基础架构/生产支持来调整和调整实时环境,而无需回到开发人员手中并通过测试(他们只想移动组件就需要更高的成本)。
概括
IoC的外部配置的主要好处来自赋予其他人(非开发人员)配置应用程序的能力,以我的经验,这仅在少数情况下才有用:
- 应用程序分发到环境会有所不同的多个站点/客户端。
- 对生产环境和设置的有限的开发控制/输入。
- 测试方案。
在实践中,我发现即使开发出我们可以控制其运行的环境,但随着时间的流逝,最好还是让其他人来更改配置:
- 在开发时,我们不知道它何时会更改(该应用程序非常有用,公司将其出售给其他人)。
- 我不想每次都需要进行轻微的更改就停留在更改代码上,而可以通过设置和使用良好的配置模型来处理。
注意:应用程序指的是完整的解决方案(不仅仅是可执行文件),因此应用程序运行所需的所有文件。
每当我们可以将代码更改为数据时,我们都朝着正确的方向迈出了一步。
将任何内容编码为数据意味着代码本身更加通用和可重用。这也意味着我们可以使用完全适合语言来指定数据。
而且,可以将XML文件读入GUI或者其他工具中,并易于进行实用的操作。我们将如何使用代码示例来做到这一点?
我一直在考虑大多数人将代码实现为数据的事情,这使得剩下的代码更加干净。我发现人们将无法以代码而不是数据来创建菜单,这是难以想象的。很显然,由于存在样板,因此在代码中执行菜单是完全错误的。
通常,重要的一点是编写程序后,谁在更改配置。使用代码中的配置,我们隐式地假设更改它的人与原始作者具有相同的技能和对源代码的访问权限。
在生产系统中,将设置的某些子集(例如示例中的年龄)提取到XML文件中并允许例如系统管理员或者支持人员可以更改值,而无需完全赋予他们源代码或者其他设置的全部权力,或者只是为了使其与复杂性隔离。
在XML配置文件中初始化将简化与将应用程序部署在其计算机上的客户端的调试/调整工作。 (因为它不需要重新编译+替换二进制文件)