104

我了解依赖注入本身的好处。让我们以 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 已经有一段时间了,我无法提供这样的例子。我可以展示单行代码来展示我使用的 hibernate、dwr 和其他框架的好处。


更新 2:
我意识到可以在不重新编译的情况下更改 IoC 配置。这真的是个好主意吗?我可以理解何时有人想要更改数据库凭据而不重新编译 - 他可能不是开发人员。在您的实践中,开发人员以外的其他人多久更改一次 IoC 配置?我认为对于开发人员而言,无需重新编译该特定类而不是更改配置。对于非开发人员,您可能希望让他的生活更轻松并提供一些更简单的配置文件。


更新 3:

接口及其具体实现之间映射的外部配置

让它外接有什么好处?您不必将所有代码都放在外部,虽然您绝对可以 - 只需将其放在 ClassName.java.txt 文件中,即时手动读取和编译 - 哇,您避免了重新编译。为什么要避免编译?!

您可以节省编码时间,因为您以声明方式提供映射,而不是在程序代码中

我知道有时声明性方法可以节省时间。例如,我只声明一次 bean 属性和 DB 列之间的映射,hibernate 在加载、保存、构建基于 HSQL 的 SQL 等时使用此映射。这就是声明性方法的工作原理。在 Spring 的情况下(在我的示例中),声明有更多的行并且与相应的代码具有相同的表现力。如果有这样的声明比代码短的例子 - 我想看看。

控制反转原则允许简单的单元测试,因为您可以用假的实现替换真实的实现(比如用内存中的替换 SQL 数据库)

我确实理解控制反转的好处(我更喜欢将这里讨论的设计模式称为依赖注入,因为 IoC 更通用 - 有多种控制,我们只反转其中一种 - 初始化控制)。我在问为什么有人需要编程语言以外的东西。我绝对可以使用代码用假的实现替换真实的实现。这段代码将表达与配置相同的内容——它只会用假值初始化字段。

mary = new FakeFemale();

我确实了解 DI 的好处。我不明白与配置相同的代码相比,外部 XML 配置有什么好处。我认为不应该避免编译——我每天都在编译,而且我还活着。我认为 DI 的配置是声明性方法的坏例子。如果声明一次并且以不同的方式多次使用,则声明可能很有用 - 例如 hibernate cfg,其中 bean 属性和 DB 列之间的映射用于保存、加载、构建搜索查询等。Spring DI 配置可以很容易地转换为配置代码,就像在这个问题的开头一样,不能吗?而且它仅用于 bean 初始化,不是吗?这意味着声明性方法不会在此处添加任何内容,是吗?

当我声明hibernate映射时,我只是给hibernate一些信息,它基于它工作——我不告诉它做什么。在春天的情况下,我的声明告诉春天到底要做什么——那么为什么要声明它,为什么不直接去做呢?


最后更新:
伙计们,很多答案都在告诉我有关依赖注入的信息,我知道这很好。问题是关于 DI 配置的目的而不是初始化代码 - 我倾向于认为初始化代码更短更清晰。到目前为止,我对我的问题的唯一答案是,当配置更改时,它可以避免重新编译。我想我应该发布另一个问题,因为这对我来说是一个很大的秘密,为什么在这种情况下应该避免编译。

4

16 回答 16

41

对我自己来说,使用 IoC(并利用外部配置)的主要原因之一是围绕以下两个方面:

  • 测试
  • 生产维护

测试

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

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

对于最后两个测试场景(集成和黑盒),您要做的是不重新编译应用程序的任何部分。

如果您的任何测试场景需要您更改配置(即:使用另一个组件来模拟银行集成,或执行性能负载),这可以很容易地处理(这确实属于配置 DI 端的好处)国际奥委会虽然。

此外,如果您的应用程序用于多个站点(具有不同的服务器和组件配置)或在实时环境中具有更改的配置,您可以使用测试的后期阶段来验证应用程序是否能够处理这些更改。

生产

作为开发人员,您没有(也不应该)控制生产环境(特别是当您的应用程序分发给多个客户或单独的站点时),这对我来说是同时使用 IoC 和外部配置的真正好处,因为这取决于基础设施/生产支持来调整和调整实时环境,而无需返回给开发人员并通过测试(当他们想要做的只是移动组件时成本更高)。

概括

IoC 的外部配置的主要好处来自赋予其他人(非开发人员)配置您的应用程序的权力,根据我的经验,这仅在有限的情况下有用:

  • 应用程序被分发到环境不同的多个站点/客户端。
  • 对生产环境和设置的开发控制/输入有限。
  • 测试场景。

在实践中,我发现即使在开发某些您确实可以控制它将运行的环境的东西时,随着时间的推移,最好让其他人能够更改配置:

  • 开发时,您不知道它何时会改变(该应用程序非常有用,您的公司将其出售给其他人)。
  • 我不想在每次请求轻微更改时都更改代码,而这些更改可以通过设置和使用良好的配置模型来处理。

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

于 2008-10-08T12:20:02.153 回答
14

依赖注入是一种编码风格,其根源在于对象委托通常是比对象继承更有用的设计模式(即对象具有关系比对象是关系更有用)。然而,另一个要素是 DI 工作所必需的,即创建对象接口。结合这两种强大的设计模式,软件工程师很快意识到他们可以创建灵活的松散耦合代码,因此依赖注入的概念诞生了。然而,直到对象反射在某些高级语言中可用时,DI 才真正起飞。反射组件是当今大部分时间的核心

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

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

  1. DI 代码更容易重用,因为“依赖”功能被外推到定义良好的接口中,允许将配置由合适的应用程序平台处理的单独对象随意插入其他对象。
  2. DI 代码更容易测试。可以通过构建实现应用程序逻辑预期接口的“模拟”对象,在黑盒中测试对象表达的功能。
  3. DI 代码更灵活。它天生就是松散耦合的代码——到了极点。这允许程序员根据对象在一端的所需接口和在另一端表示的接口来挑选和选择对象的连接方式。
  4. DI 对象的外部 (Xml) 配置意味着其他人可以在不可预见的方向自定义您的代码。
  5. 外部配置也是一种关注点分离模式,因为对象初始化和对象相互依赖管理的所有问题都可以由应用服务器处理。
  6. 请注意,使用 DI 模式不需要外部配置,对于简单的互连,一个小的构建器对象通常就足够了。两者之间需要权衡灵活性。构建器对象不像外部可见的配置文件那样灵活。DI 系统的开发人员必须权衡灵活性与便利性的优势,注意配置文件中表达的对对象构造的小规模、细粒度控制可能会增加混乱和维护成本。

绝对 DI 代码似乎更麻烦,将所有用于配置对象的 XML 文件注入其他对象的缺点似乎很困难。然而,这是 DI 系统的重点。您将代码对象作为一系列配置设置混合和匹配的能力允许您使用 3rd 方代码构建复杂的系统,而您的代码最少。

问题中提供的示例仅涉及正确分解的 DI 对象库可以提供的表达能力的表面。通过一些实践和大量自律,大多数 DI 从业者发现他们可以构建对应用程序代码具有 100% 测试覆盖率的系统。仅这一点就非同寻常。这不是几百行代码的小型应用程序的 100% 测试覆盖率,而是包含数十万行代码的应用程序的 100% 测试覆盖率。我无法描述提供这种可测试性级别的任何其他设计模式。

你是对的,一个仅仅 10 行代码的应用程序比几个对象加上一系列 XML 配置文件更容易理解。然而,与大多数强大的设计模式一样,随着您继续向系统添加新功能,您会发现收益。

简而言之,基于 DI 的大规模应用程序更易于调试且更易于理解。虽然 Xml 配置没有“编译时检查”,但作者知道的所有应用程序服务如果他们试图将具有不兼容接口的对象注入另一个对象,都会向开发人员提供错误消息。并且大多数提供了涵盖所有已知对象配置的“检查”功能。通过检查要注入的对象 A 是否实现了对象 B 对所有已配置的对象注入所需的接口,可以轻松快速地完成此操作。

于 2008-09-25T08:21:37.907 回答
7

这是一个有点牵强的问题,但我倾向于同意大量的 xml 配置并没有真正带来太多好处。我希望我的应用程序对依赖项尽可能轻,包括庞大的框架。

它们在很多时候简化了代码,但它们也有复杂性的开销,这使得跟踪问题变得相当困难(我亲眼看到了这样的问题,而直接的 Java 处理起来会更舒服)。

我想这在某种程度上取决于风格,以及您对什么感到满意...您是否喜欢使用自己的解决方案并从内而外地了解它,或者依靠现有的解决方案,而这些解决方案在配置不正确时可能会被证明是困难的? t 刚刚好?这都是一个权衡。

然而,XML 配置是我的一个小烦恼……我不惜一切代价尽量避免它。

于 2008-09-25T07:44:53.563 回答
5

任何时候您可以将代码更改为数据,您就朝着正确的方向迈出了一步。

将任何内容编码为数据意味着您的代码本身更通用且可重用。这也意味着您的数据可以用完全适合它的语言来指定。

此外,可以将 XML 文件读入 GUI 或其他工具,并且可以轻松地进行实用操作。您将如何使用代码示例来做到这一点?

我一直在将大多数人会以代码形式实现的东西分解到数据中,这使得留下的代码更加干净。我发现人们会用代码而不是数据来创建菜单是不可思议的——很明显,由于样板文件,在代码中这样做是完全错误的。

于 2009-04-15T19:23:08.233 回答
3

使用 DI 容器的原因是您不必在代码中预先配置十亿个简单的 getter 和 setter 属性。你真的想用 new X() 硬编码所有这些吗?当然,你可以有一个默认值,但是 DI 容器允许创建单例,这非常容易,并且允许你专注于代码的细节,而不是初始化它的杂项任务。

例如,Spring 允许您实现 InitializingBean 接口并添加 afterPropertiesSet 方法(您也可以指定“init-method”以避免将代码与 Spring 耦合)。这些方法将允许您确保在启动时正确配置类实例中指定为字段的任何接口,然后您不再需要对 getter 和 setter 进行空值检查(假设您确实允许单例保持线程安全)。

此外,使用 DI 容器进行复杂的初始化比自己进行要容易得多。例如,我协助使用 XFire(不是 CeltiXFire,我们只使用 Java 1.4)。该应用程序使用了 Spring,但不幸的是它使用了 XFire 的 services.xml 配置机制。当一个元素集合需要声明它有零个或多个实例而不是一个或多个实例时,我必须重写为这个特定服务提供的一些 XFire 代码。

在其 Spring beans 模式中定义了某些 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 容器中提供并且您只是更改它们以适应特殊情况时,处理代码中的怪癖要干净得多,这经常发生。

于 2008-10-01T02:12:36.563 回答
3

我有你的答案

显然,每种方法都需要权衡取舍,但外部化的 XML 配置文件对于企业开发很有用,其中构建系统用于编译代码而不是您的 IDE。使用构建系统,您可能希望将某些值注入您的代码 - 例如构建版本(每次编译时必须手动更新可能会很痛苦)。当您的构建系统从某些版本控制系统中提取代码时,痛苦会更大。在编译时修改简单值将需要您更改文件、提交、编译,然后每次更改都恢复。这些不是您要提交到版本控制中的更改。

关于构建系统和外部配置的其他有用用例:

  • 为不同构建的单个代码库注入样式/样式表
  • 为您的单个代码库注入不同的动态内容集(或对它们的引用)
  • 为不同的构建/客户端注入本地化上下文
  • 将 Web 服务 URI 更改为备用服务器(当主服务器出现故障时)

更新:上面所有的例子都是关于不一定需要依赖类的东西。但是您可以轻松构建同时需要复杂对象和自动化的案例 - 例如:

  • 想象一下,您有一个系统可以监控您网站的流量。根据并发用户的数量,它打开/关闭日志记录机制。也许当机制关闭时,一个存根对象被放置在它的位置。
  • 想象一下,您有一个网络会议系统,根据用户的数量,您希望根据参与者的数量来切换 P2P 的能力
于 2011-02-24T01:35:08.720 回答
2

每次更改配置时都不需要重新编译代码。它将简化程序的部署和维护。例如,您只需在配置文件中进行 1 次更改即可将一个组件与另一个组件交换。

于 2008-09-25T07:43:52.053 回答
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 接口)

于 2008-09-25T08:02:59.213 回答
1

在 .NET 世界中,大多数 IoC 框架都提供 XML 和代码配置。

例如,StructureMap 和 Ninject 使用 fluent 接口来配置容器。您不再受限于使用 XML 配置文件。Spring 也存在于 .NET 中,由于它是他历史上的主要配置接口,因此严重依赖 XML 文件,但仍然可以通过编程方式配置容器。

于 2008-09-25T07:43:34.237 回答
1

易于将部分配置组合成最终的完整配置。

例如,在 Web 应用程序中,模型、视图和控制器通常在单独的配置文件中指定。使用声明式的方式,可以加载,例如:

  UI-context.xml
  Model-context.xml
  Controller-context.xml

或者加载不同的 UI 和一些额外的控制器:

  AlternateUI-context.xml
  Model-context.xml
  Controller-context.xml
  ControllerAdditions-context.xml

在代码中做同样的事情需要一个用于组合部分配置的基础设施。在代码中并非不可能,但使用 IoC 框架肯定更容易做到。

于 2008-09-26T04:01:54.247 回答
1

通常,重要的一点是在编写程序后更改配置。通过代码中的配置,您隐含地假设更改它的人具有与原始作者相同的技能和对源代码等的访问权限。

在生产系统中,将一些设置子集(例如您的示例中的年龄)提取到 XML 文件并允许例如系统管理员或支持人员更改值而不赋予他们对源代码或其他设置的全部权力 - 或者只是将它们与复杂性隔离开来。

于 2010-06-17T22:22:29.517 回答
1

从 Spring 的角度来看,我可以给你两个答案。

首先,XML 配置不是定义配置的唯一方法。大多数事情都可以使用注释进行配置,而必须使用 XML 完成的事情是配置您无论如何都不会编写的代码,例如您从库中使用的连接池。Spring 3 包括一种使用 Java 定义 DI 配置的方法,类似于您示例中的手动 DI 配置。所以使用 Spring 并不意味着你必须使用基于 XML 的配置文件。

其次,Spring 不仅仅是一个 DI 框架。它具有许多其他功能,包括事务管理和 AOP。Spring XML 配置将所有这些概念混合在一起。通常在同一个配置文件中,我指定 bean 依赖项、事务设置并添加在后台使用 AOP 实际处理的会话范围 bean。我发现 XML 配置提供了一个更好的地方来管理所有这些特性。我还觉得基于注释的配置和 XML 配置比基于 Java 的配置扩展得更好。

But I do see your point and there isn't anything wrong with defining the dependency injection configuration in Java. I normally do that myself in unit tests and when I'm working on a project small enough that I haven't added a DI framework. I don't normally specify configuration in Java because to me that's the kind plumbing code that I'm trying to get away from writing when I chose to use Spring. That's a preference though, it doesn't mean that XML configuration is superior to Java based configuration.

于 2011-04-05T21:04:20.220 回答
0

Spring 也有一个属性加载器。我们使用这种方法来设置依赖于环境的变量(例如开发、测试、验收、生产……)。例如,这可能是要收听的队列。

如果没有理由改变属性,也没有理由以这种方式配置它。

于 2008-09-25T07:45:13.623 回答
0

您的案例非常简单,因此不需要像 Spring 这样的 IoC(控制反转)容器。另一方面,当你“编程到接口,而不是实现”(这是 OOP 中的一个很好的做法)时,你可以有这样的代码:

IService myService;
// ...
public void doSomething() {
  myService.fetchData();
}

(注意 myService 的类型是 IService ——一个接口,而不是一个具体的实现)。现在让您的 IoC 容器在初始化期间自动提供正确的 IService 具体实例会很方便——当您有许多接口和许多实现时,手动执行此操作可能很麻烦。IoC 容器(依赖注入框架)的主要好处是:

  • 接口及其具体实现之间映射的外部配置
  • IoC 容器处理一些棘手的问题,例如解决复杂的依赖关系图、管理组件的生命周期等。
  • 您可以节省编码时间,因为您以声明方式提供映射,而不是在程序代码中
  • 控制反转原则允许简单的单元测试,因为您可以用假的实现替换真实的实现(比如用内存中的替换 SQL 数据库)
于 2008-09-25T07:51:11.770 回答
0

在 XML 配置文件中进行初始化将简化您与将您的应用程序部署在其计算机上的客户端的调试/调整工作。(因为它不需要重新编译+二进制文件替换)

于 2010-11-12T15:58:36.710 回答
-2

最吸引人的原因之一是“好莱坞原则”:不要打电话给我们,我们会打电话给你。组件本身不需要查找其他组件和服务;相反,它们会自动提供给它。在 Java 中,这意味着不再需要在组件内部进行 JNDI 查找。

单独对组件进行单元测试也容易得多:您无需为其提供所需组件的实际实现,而只需使用(可能是自动生成的)模拟。

于 2008-09-25T08:54:46.407 回答