依赖注入容器有哪些好处?

105

我理解依赖注入本身的好处。以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的人理解呢?那么依赖注入框架的主要目的是什么呢?(或者展示其优点的代码片段。)


UPDATE:
对于...

IService myService;// ...
public void doSomething() {  
  myService.fetchData();
}
如果有多个myService的实现,IoC框架如何猜测我想注入哪个?如果只有一个给定接口的实现,并且我让IoC容器自动决定使用它,那么当第二个实现出现时,就会出现问题。如果有意只有一种可能的接口实现,那么就不需要注入它。
展示IoC的配置小片段,这将显示其好处。我已经使用Spring一段时间了,但我无法提供这样的示例。我可以展示单行代码,演示我使用的其他框架(如Hibernate,DWR等)的好处。
我意识到IoC配置可以在不重新编译的情况下更改。这真的是个好主意吗?我可以理解有人希望在不重新编译的情况下更改数据库凭据 - 他可能不是开发人员。在您的实践中,有多少人会更改IoC配置而不是开发人员?我认为对于开发人员来说,重新编译那个特定的类而不是更改配置并没有太大的工作量。对于非开发人员,您可能希望让他们的生活更轻松,并提供一些更简单的配置文件。
外部配置接口和它们的具体实现之间的映射是怎么回事?你不会使所有的代码都外部化,尽管你肯定可以 - 只需将它放在ClassName.java.txt文件中,手动读取和编译 - wow,你避免了重新编译。为什么要避免编译?
您提供映射声明后,节省编码时间,而不是在过程式代码中进行显式的映射。我理解有时候声明性方法可以节省时间,例如,我只在一个bean属性和一个DB列之间声明一次映射,然后Hibernate在加载、保存、基于HSQL建立SQL等时使用这个映射。这就是声明式方法起作用的地方。在Spring(我的例子中),声明需要更多行,并且与相应的代码具有相同的表现力。如果有一个示例可以使这种声明短于代码 - 我想看看。
控制反转原则允许轻松进行单元测试,因为您可以将真实实现替换为伪实现(如将SQL数据库替换为内存数据库)。我确实理解控制反转的好处(我更喜欢称所讨论的设计模式为依赖注入,因为IoC更通用 - 有许多种控制,我们只颠倒其中之一 - 初始化控制)。我问的是为什么有人需要使用其他东西来代替编程语言。我绝对可以使用代码将真实实现替换为伪实现。这段代码将表达与配置相同的内容 - 它只是使用伪值初始化字段。
mary = new FakeFemale();

我理解依赖注入(DI)的好处,但我不明白相比于配置代码而言,外部XML配置有何优势。我认为编译不应该被避免——我每天都进行编译,而且我还活着。我认为DI的配置是声明式方法的糟糕示例。如果声明只需声明一次并可以以多种方式使用,那么声明可以很有用,就像hibernate cfg一样,其中bean属性和DB列之间的映射用于保存、加载、构建搜索查询等等。Spring DI配置可以轻松地转换为配置代码,就像本问题开头所述的那样,难道不能吗?它只用于bean初始化,不是吗?这意味着在这里,声明式方法并没有增加任何东西,对吧?

当我声明hibernate映射时,我只是向hibernate提供一些信息,它就能基于此工作——我不会告诉它该做什么。在spring的情况下,我的声明告诉spring确切应该做什么——那么为什么要声明它,为什么不直接去做呢?


最近更新:
大家,很多回答告诉我依赖注入的好处,我知道依赖注入是好的。 问题是关于DI配置的目的,而不是初始化代码——我倾向于认为初始化代码更短、更清晰。 到目前为止我得到的唯一回答是,它避免了在配置更改时重新编译。我想我应该发布另一个问题,因为对于我来说,为什么在这种情况下应该避免编译还是个大谜团。


21
终于有人有勇气问这个问题了。确实,为什么你要避免重新编译,而以牺牲(或至少降低)工具/集成开发环境的支持为代价来改变你的实现呢? - Christian Klauser
3
标题似乎不太准确。作者说IOC容器还不错,但好像对使用XML配置而不是通过代码进行配置有意见(这也很合理)。我建议可能改为“通过XML或其他非代码方式配置IOC容器有哪些好处?” - Orion Edwards
我提供的@Orion示例(包括男性和女性)不需要任何IOC容器。我对IOC感到满意;无论是使用XML配置的容器还是其他方式,都仍然是一个未决问题。 - Pavel Feldman
@Orion 2:虽然我在大多数项目中都使用某种形式的IOC,但有些项目像变量分配容器或if语句容器一样从IOC容器中获益良多-只要语言足够好就可以了。我没有重新编译正在工作的项目,把开发/测试/生产初始化代码方便地分离出来的问题,对我来说这个标题是可以接受的。 - Pavel Feldman
我发现样例有问题。其中一个原则是注入服务,而不是数据 - Jacek Cz
16个回答

41

对于我个人而言,使用IoC(并利用外部配置)的主要原因之一是涉及以下两个方面:

  • 测试
  • 生产维护

测试

如果您将测试分为3个场景(在大规模开发中相当常见):

  1. 单元测试
  2. 集成测试
  3. 黑盒测试

您将希望在最后两个测试场景(集成和黑盒)中不重新编译应用程序的任何部分。

如果您的任何测试场景需要更改配置(例如:使用另一个组件来模拟银行集成或执行性能负载),则可以轻松处理此问题(这确实属于配置IoC的DI方面的好处)。

此外,如果您的应用程序在多个站点上使用(具有不同的服务器和组件配置)或在现场环境中具有不断变化的配置,则可以使用后期测试来验证应用程序将处理这些更改。

生产

作为开发人员,您不应该(也不应该)控制生产环境(特别是当您的应用程序被分发给多个客户或单独的站点时),这对我来说是使用IoC和外部配置的真正好处,因为基础架构/生产支持可以调整和调整实时环境,而无需回到开发人员并通过测试(当他们想要移动组件时成本更高)。
总结
外部配置IoC的主要优点在于让其他人(非开发人员)有能力配置您的应用程序,在我的经验中,这仅在有限的一组情况下有用:
应用程序分布在将存在差异的多个站点/客户端上。 生产环境和设置的开发控制/输入有限。 测试场景。
实际上,我发现即使在开发某些您可以控制其运行环境的东西时,随着时间的推移,最好还是让其他人具备更改配置的能力。
  • 在开发过程中,您不知道何时会更改(应用程序非常有用,您的公司将其出售给其他人)。
  • 我不想被困在每次请求轻微更改时都必须更改代码的境地中,这些更改可以通过设置和使用良好的配置模型来处理。

注意:应用程序指完整的解决方案(不仅仅是可执行文件),因此需要所有运行应用程序所需的文件。


15

依赖注入(Dependency Injection)是一种编程风格,源于对对象委派(object delegation)通常比对象继承(即对象 is-a 关系)更有用的设计模式的观察。然而,让 DI 生效还需要另一个要素,那就是创建对象接口。将这两个强大的设计模式相结合,软件工程师很快意识到他们可以创建灵活、松耦合的代码,因此依赖注入的概念应运而生。然而,直到某些高级语言中出现了对象反射技术,DI 才真正起飞。反射组件是当今大多数 DI 系统核心的一部分,因为 DI 的真正酷炫之处需要通过一种系统外部的、独立于对象本身的方法来编程选择对象并对其进行配置和注入。

一种语言必须为普通的面向对象编程技术提供良好的支持,同时也要支持对象接口和对象反射(例如 Java 和 C#)。虽然您可以在 C++ 系统中使用 DI 模式构建程序,但语言本身缺乏反射支持,从而阻止了它支持应用服务器和其他 DI 平台,因此限制了 DI 模式的表达能力。

使用 DI 模式构建的系统的优点:

  1. 依赖注入代码更容易被重用,因为“所依赖的”功能被抽象成了明确定义的接口,允许由适当的应用程序平台处理其配置的独立对象随意插入到其他对象中。
  2. 依赖注入代码更容易测试。通过构建实现应用程序逻辑预期的接口的“模拟”对象,可以测试对象所表达的功能。
  3. 依赖注入代码更灵活。它是天然的松耦合代码,甚至可以说是非常极端的。这使得程序员可以根据一个端点上所需的接口和另一个端点上所表达的接口来挑选和选择对象之间的连接方式。
  4. DI 对象的外部(Xml)配置意味着其他人可以以不可预见的方向定制您的代码。
  5. 外部配置也是关注点分离模式,因为对象初始化和对象相互依赖管理的所有问题都可以由应用服务器处理。
请注意,使用DI模式并不需要外部配置,对于简单的互连,一个小的生成器对象通常就足够了。这两者之间存在灵活性权衡。与外部可见的配置文件相比,构建器对象并不是一种那么灵活的选择。DI系统的开发者必须权衡灵活性和便利性的优劣,同时需要注意,表达在配置文件中的小规模、精细的对象构建控制可能会增加混乱和维护成本。
DI代码似乎更加繁琐,拥有所有配置对象注入到其他对象的XML文件的不利之处似乎很棘手。然而,这正是DI系统的重点。你可以将混合和匹配的代码对象作为一系列配置设置来使用,从而在最小化自己编码的情况下构建复杂的系统,使用第三方代码。
提供的问题示例仅触及适当分解的DI对象库所提供的表达能力的表面。通过一些实践和大量的自我约束,大多数DI从业者发现他们可以构建具有100%测试覆盖率的应用程序代码系统。这一点单独来看是非凡的。这不是针对几百行代码的小应用程序的100%测试覆盖率,而是针对包含数十万行代码的应用程序的100%测试覆盖率。我无法描述任何其他设计模式能够提供这种级别的可测试性。
你是正确的,仅有几十行代码的应用程序比几个对象加上一系列XML配置文件更容易理解。然而就像大多数强大的设计模式一样,随着你继续向系统添加新功能,所得到的收益会越来越大。
简而言之,基于DI的大规模应用程序既易于调试又易于理解。虽然Xml配置文件不是“编译时检查”的,但是本作者所知道的所有应用程序服务都将为开发人员提供错误消息,如果他们试图将具有不兼容接口的对象注入到另一个对象中,则会提供错误消息。并且大多数提供了一个“检查”功能,涵盖了所有已知对象的配置。这可以通过检查要注入的对象A是否实现了所有已配置对象注入所需求的对象B接口来轻松快速地完成。

4
理解DI(依赖注入)的好处。我不理解外部XML配置相对于编写执行相同操作的代码所增加的优势。您提到的好处是由DI设计模式提供的。问题是关于与普通初始化代码相比,DI配置的好处。 - Pavel Feldman
外部配置也是分离的关键...配置分离是依赖注入的核心,非常好。并且可以使用初始化代码来完成。相比于初始化代码,配置文件添加了什么?对我来说,似乎每行配置文件都有相应的初始化代码行。 - Pavel Feldman

7
这是一个有点复杂的问题,但我倾向于认为大量的XML配置并不能带来很多好处。我喜欢我的应用程序尽可能轻依赖,包括庞大的框架。
它们确实简化了代码,但也增加了复杂性的开销,这使得跟踪问题变得相当困难(我亲眼见过这样的问题,如果是纯Java,我会更舒适地处理)。
我想这有点取决于风格和你对什么感到舒适...你喜欢自己解决问题,并且知道它内部的好处,还是依赖现有的解决方案,当配置不正确时可能会变得棘手?这都是一种权衡。
然而,XML配置是我讨厌的一个小事...我尽量避免使用它。

5
任何时候,只要您将代码转化为数据,您就朝着正确的方向迈出了一步。
将任何内容编码为数据意味着您的代码本身更加通用和可重用。这也意味着您的数据可以以完全适合其的语言进行指定。
此外,XML文件可以被读入GUI或其他工具中并且可以很容易地进行实际操作。使用代码示例该怎么做呢?
我不断地将大多数人会将其实现为代码的事物分解成数据,这使得剩下的代码变得更加简洁。我认为,人们在代码中创建菜单而不是作为数据几乎是不可思议的——由于样板文件,这显然是错误的。

有道理,我没有从这个角度考虑过。 - Pavel Feldman
7
然而,人们常常走另一条路,试图将逻辑放入数据中,这只会导致您用次标准的编程语言编写应用程序。 - Casebash
@Casebash 这是一个有趣的观点 - 我对示例非常感兴趣。我发现,任何我可以转化为数据的东西都很有帮助。我还发现,如果我只做你所说的事情,语言实际上会得到改进,因为它是一种DSL - 但即使如此,创建一个全新的语言也需要严肃的理由。 - Bill K
1
任何时候,只要你能将代码转换为数据,你就朝着正确的方向迈出了一步。欢迎来到软编码反模式。 - Raedwald
@Raedwald,你所说的是外部化,如果你不知道自己在做什么,这可能会非常困难(而且有些无能的人试图这样做,失败了并称其为反模式)。更积极的例子包括注入、迭代器、几乎所有带有注释的内容以及使用数组初始化的任何内容。大多数优秀的编程结构都是试图将代码中的差异分离出来,并将剩下的内容组合起来,用可以更好地分组和管理的东西来驱动它。 - Bill K

3
我有你的答案。
显然,每种方法都有权衡取舍,但外部化的XML配置文件对于企业开发非常有用,因为构建系统用于编译代码而不是你的IDE。使用构建系统,您可能希望将某些值注入到代码中-例如构建的版本(手动更新每次编译可能很痛苦)。当构建系统从某个版本控制系统中提取代码时,这种痛苦更大。在编译时修改简单值需要您更改文件,提交它,编译,然后每次更改都还原。这些不是您想提交到版本控制的更改。
其他有关构建系统和外部配置的有用用例:
- 为不同构建注入样式/样式表 - 为单个代码库注入不同集合的动态内容(或对它们的引用) - 为不同的构建/客户端注入本地化上下文 - 将Web服务URI更改为备份服务器(当主服务器关闭时)
更新: 以上所有示例都是针对不一定需要依赖类的事物。但是,您可以轻松构建出既需要复杂对象又需要自动化的情况-例如:
- 想象一下,您拥有一个系统,它监视您网站的流量。根据并发用户的数量,它打开/关闭日志记录机制。也许在机制关闭时,会将存根对象放在其位置。 - 想象一下,您拥有一个Web会议系统,根据用户数量,您希望根据参与者人数切换执行P2P的能力。

在最顶部突出企业方面的重要性真的很重要。测试写得很差的遗留代码有时可能会是长达数天的噩梦。 - Richard Le Mesurier

3
使用 DI 容器的原因是你不需要在代码中预先配置数十个属性,这些属性只是 getter 和 setter。难道你真的想要使用 new X() 硬编码它们吗?当然,你可以使用默认值,但 DI 容器允许创建单例对象,这非常容易,并且使你专注于代码的细节,而不是初始化它的杂项任务。
例如,Spring 允许你实现 InitializingBean 接口并添加 afterPropertiesSet 方法(你也可以指定 "init-method" 以避免将代码与 Spring 耦合)。这些方法将确保任何在类实例中指定为字段的接口在启动时正确配置,然后你就不再需要对 getters 和 setters 进行空值检查(假设你允许单例对象保持线程安全)。
此外,使用 DI 容器比自己进行复杂初始化要容易得多。例如,我帮助使用 XFire(不是 CeltiXFire,我们只使用 Java 1.4)。该应用程序使用了 Spring,但不幸的是使用了 XFire 的 services.xml 配置机制。当一个元素的集合需要声明其有零个或多个实例而不是一个或多个实例时,我必须覆盖该特定服务的一些提供的 XFire 代码。
在 XFire 的 Spring bean 模式中定义了某些默认值。因此,如果我们使用 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容器中提供并且您只需更改它们以适应特殊情况时,处理代码中的怪癖要干净得多,这种情况经常发生。

2
您不需要每次更改配置时重新编译代码。这将简化程序部署和维护。例如,您可以通过仅更改配置文件中的一个设置来替换一个组件。

部署?可能是... 部署的维护?可能是... 代码的维护?我倾向于认为不是... 通过框架进行调试往往会带来很大的麻烦,而POJO在这方面处理起来要容易得多。 - Mike Stone
1
Mike,我没有提到代码。我们都知道XML配置很糟糕 :) - aku
嗯...你有多经常更改组件而不重新编译,以及为什么要这样做?我理解如果有人更改了数据库凭据并且不想重新编译程序-他可能不是开发人员。但我几乎无法想象除开发人员之外的其他人更改Spring配置。 - Pavel Feldman
通常这种情况发生在需要将程序部署到数百个客户端的场景下。在这种情况下,更改配置要比部署产品的新版本容易得多。你说得对,开发人员通常会创建新的配置文件,由管理员进行部署。 - aku

2
你可以插入一个新的实现来替换女朋友。这样,你就不需要重新编译代码就可以注入新的女性。
<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接口)

为什么在不重新编译的情况下进行逻辑修改被认为是一个好主意? - Pavel Feldman
我绝对可以这么做: HotFemale jane = new HotFmale(); jane.setAge(19); john.setGirlfriend(jane); 所以唯一不同的就是cfg可以在不重新编译的情况下改变?这似乎是Spring讨论时的常见答案。为什么?!为什么避免编译会很好呢? - Pavel Feldman
我可以更好地测试代码,我可以模拟女性对象。 - Paul Whelan
@Pavel Feldman:因为如果您已经在客户端部署了应用程序,这将更容易。 - Andrei Rînea

1
从Spring的角度来看,我可以给你两个答案。
首先,XML配置并不是定义配置的唯一方式。大多数东西都可以使用注释进行配置,必须使用XML完成的事情是配置你根本没有编写的代码,比如从库中使用的连接池。Spring 3包括一种使用Java定义DI配置的方法,类似于你示例中手动编写的DI配置。因此,使用Spring并不意味着您必须使用基于XML的配置文件。
其次,Spring远不止是一个DI框架。它有许多其他功能,包括事务管理和AOP。Spring XML配置将所有这些概念混合在一起。通常在同一个配置文件中,我会指定bean依赖项、事务设置,并添加实际上使用AOP处理的会话范围bean。我发现XML配置提供了更好的管理所有这些功能的地方。我还觉得基于注释的配置和XML配置比使用基于Java的配置更具可扩展性。

但我确实理解你的观点,在Java中定义依赖注入配置并没有什么问题。在单元测试和我正在处理的项目足够小而我还没有添加DI框架时,我通常会自己这样做。我通常不会在Java中指定配置,因为对我来说,那是我试图避免编写的管道代码类型,当我选择使用Spring时。这只是一种偏好,这并不意味着XML配置优于基于Java的配置。


1

通常,重要的是在程序编写后由谁更改配置。使用代码中的配置,您会默认假设更改它的人具有与原始作者相同的技能和访问源代码等。

在生产系统中,将某些设置的子集(例如您的示例中的年龄)提取到XML文件中非常实用,并允许系统管理员或支持人员更改值,而不会给他们完全控制源代码或其他设置 - 或仅将其与复杂性隔离开来。


这是一个很有道理的观点,但Spring配置经常很复杂。虽然更改年龄很容易,但系统管理员仍然必须处理他不完全理解的大型XML文件。难道不应该将应该配置的部分提取出来,放入比Spring XML配置更简单的东西中吗?比如说属性文件,只有一行“age=23”,而不让管理员更改其他需要了解内部程序结构的详细信息,如类名等。 - Pavel Feldman
我最近在一个项目中工作,该项目混合了Java代码和XSLT。 团队成员既有在Java方面强大(可能对使用XML和XSLT不太熟悉)的人,又有非常擅长使用XML和XSLT(但对Java不太熟悉)的人。 由于第二组将管理配置,因此使用Spring并具有XML配置是有意义的。 换句话说,Spring解决了团队中的分工问题。 它没有解决“技术”问题; 就是说,配置也可以使用Java代码轻松完成。 - Dawood ibn Kareem
“支持人员”什么时候会知道必须更改依赖注入容器中创建的某些类??真的以为这是开发人员的工作吗? - Jimbo
这正是提取配置值(例如,您集成的系统的URL)有意义的原因:支持人员编辑属性文件或(在最坏的情况下)XML文件,编译后的Java类保持不变。 - Miro A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接