【问题标题】:Understanding the need for a DI framework了解对 DI 框架的需求
【发布时间】:2010-10-04 18:40:57
【问题描述】:

这可能是一个幼稚的问题。我目前正在学习 Spring 框架和依赖注入。虽然 DI 的基本原理很容易掌握,但为什么需要一个复杂的框架来实现它并不是很明显。

考虑以下几点:

public abstract class Saw
{
    public abstract void cut(String wood);
}

public class HandSaw extends Saw
{
    public void cut(String wood)
    {
        // chop it up
    }
}

public class ChainSaw extends Saw
{
    public void cut(String wood)
    {
        // chop it a lot faster
    }
}

public class SawMill
{
    private Saw saw;

    public void setSaw(Saw saw)
    {
        this.saw = saw;
    }

    public void run(String wood)
    {
        saw.cut("some wood");
    }
}

那么你可以这样做:

Saw saw = new HandSaw();
SawMill sawMill = new SawMill();
sawMill.setSaw(saw);
sawMill.run();

相当于:

<bean id="saw" class="HandSaw"/>

<bean id="sawMill" class="SawMill">
   <property name="saw" ref="saw"/>
</bean>

加:

ApplicationContext context = new ClassPathXmlApplicationContext("sawmill.xml");
SawMill springSawMill = (SawMill)context.getBean("sawMill");
springSawMill.run();

当然,这是一个人为的例子,对于更复杂的对象关系,隐藏 XML 文件可能比以编程方式编写它更有效,但肯定还有更多的东西吗?

(我知道 Spring 框架不止于此,但我正在考虑需要一个 DI 容器。)

在第一个示例中,在中途更改依赖项也很简单:

// gotta chop it faster
saw = new ChainSaw();
sawMill.setSaw(saw);
sawMill.run();

【问题讨论】:

标签: java spring dependency-injection


【解决方案1】:

我也有同样的问题,答案是这样的:
当然,你可以做你在“那么你可以简单地做:......”中描述的事情(我们称之为“A类”)。但是,这会将 A 类耦合到 HandSaw 或 SawMill 类所需的所有依赖项。为什么 A 应该与 HandSaw 耦合 - 或者,如果您采用更现实的场景,为什么我的业务逻辑应该耦合到 DAO 层所需的 JDBC 连接实现?
我当时提出的解决方案是“然后将依赖关系进一步移动” - 好的,现在我的视图与 JDBC 连接耦合,我应该只处理 HTML(或 Swing,选择你的风格)。

由 XML(或 JavaConfig)配置的 DI 框架通过让您“获得所需的服务”来解决这个问题。您不必关心它是如何初始化的,它需要什么工作 - 您只需获取服务对象并激活它。

此外,您对“加号:”(您在哪里做SawMill springSawMill = (SawMill)context.getBean("sawMill"); springSawMill.run();)有一个误解-您不需要从上下文中获取 sawMill bean-sawMill bean 应该已经注入到您的对象中(A类)由DI框架。因此,您只需使用“sawMill.run()”而不是 ...getBean(...),而不关心它来自哪里、谁初始化它以及如何初始化它。对于所有你关心的,它可以直接进入 /dev/null,或者测试输出,或者真正的 CnC 引擎......关键是 - 你不在乎。你所关心的只是你那小小的 A 班,它应该按照合同规定去做——启动锯木厂。

【讨论】:

  • 据我了解,此答案与依赖注入模式的“官方”定义 (martinfowler.com/articles/injection.html) 相矛盾。仅当需要在运行时选择实现“Saw”抽象的实际类时,与“HandSaw”类耦合的“A”类才是一个问题。否则,在客户端代码中直接实例化 Saw 实现类是完全可以的。 DI 实际上是在实际需要的情况下“将配置与使用分开”,而不是默认情况下无处不在。
  • 只有在您开始测试之前才是正确的。然后你希望你有办法将“TestSawImpl”(它只是断言)放入 A 而不是真正的“HandSaw”,这需要一堆木头、一个电源和一个经过认证的锯操作员(可能还有一个待命的医生) .
  • 不,总是如此。我可以使用模拟工具轻松编写单元测试。无需创建 TestSawImpl,因为模拟工具允许我用一行代码模拟任何 Saw 实现类,即使具体类在编译时未知。
  • 引入另一层编织/动态类加载/其他技巧以使测试工作,使它们与它们应该模拟的现实世界更加分离。确实可以做到,但我完全赞成简单的解决方案 - 有一个设置器并在您的测试中设置不同的设置。
  • 很公平,如果您更喜欢手动模拟,我可以。不过,其他人发现使用模拟 API 是更简单的解决方案。
【解决方案2】:

依赖注入是implicit parameter passing的退化形式,目的基本相同,解决所谓的The Configurations Problem

配置问题是 传播运行时偏好 在整个程序中,允许 多个并发配置集 在静态下安全共存 保证分离。

依赖注入框架弥补了语言中implicit parametersCurried functionsmonads 便利设施的不足。

【讨论】:

    【解决方案3】:

    Spring 具有三个同等重要的特性:

    1. 依赖注入
    2. 面向方面的编程
    3. 框架类库,可帮助处理持久性、远程处理、Web mvc 等。

    我同意,当您将依赖注入与对 new 的单个调用进行比较时,很难看出依赖注入的优势。在这种情况下,后者肯定会看起来更简单,因为它是一行代码。 Spring 的配置总是会增加代码行数,所以这不是一个成功的论点。

    当您可以从类中取出诸如事务之类的横切关注点并使用方面以声明性方式设置它们时,它开始看起来好多了。与单个“新”调用的比较并不是 Spring 的创建目的。

    也许使用 Spring 的最佳结果是它推荐的惯用语使用接口、分层和良好原则(如 DRY)的方式。这实际上只是 Rod Johnson 在他的咨询工作中使用的面向对象最佳实践的提炼。他发现,随着时间的推移,他积累的代码帮助他为客户提供更好的软件赚钱。他总结了他在“专家 1:1 J2EE”中的经验,并最终将代码开源为 Spring。

    如果你认为他的经验也可以帮助你编写更好的代码,我会说购买框架。

    我认为,除非您将所有这三个功能结合起来,否则您无法获得 Spring 的全部价值。

    【讨论】:

      【解决方案4】:

      当然,这是一个人为的例子,对于更复杂的对象关系,隐藏一个 XML 文件可能比以编程方式编写它更有效,但肯定还有更多的东西吗?

      我认为将“接线”放在配置文件中比在代码中手动执行更有意义,原因如下:

      1. 配置在您的代码之外。
      2. 可以简单地对外部 (XML) 文件进行布线更改(告诉您的 sawmill 使用 Saw 的不同实例),并且不需要更改代码、重新编译、重新部署等。
      3. 当你有几十个类和几层注入时(例如:你有一个web Controller 类,它获取一个包含你的业务逻辑的Service 类,它使用DAO 来获取@987654326 @s 来自数据库,其中注入了 DataSource 等),手动连接协作者很乏味,需要几十行代码,除了连接之外什么都不做。
      4. 这不是一个明确的“好处”,但通过将所有“连接”都放在代码外部,我认为它有助于向开发人员强化依赖注入的核心思想,特别是对接口进行编码的想法,而不是实现。通过手动接线,很容易回到旧的方式。

      【讨论】:

      • 我认为前 3 个可能没有 4 个那么清晰。:) 外部配置增加了间接性(复杂性);维护普通的旧 Java 代码比维护代码 XML 配置更令人愉快(尽管现代 IDE 对 Spring 有很好的支持);又回到了原来的问题……
      • 所以,依赖注入的本质就是打败类型系统?
      • 我认为外部文件降低了复杂性;信息存储在一个位置,而不是您在各处进行布线;但也许这只是我的看法
      【解决方案5】:

      我通常不关心基于 XML 或反射的 DI,因为在我的用例中,它增加了不必要的复杂性。相反,我通常会选择某种形式的手动 DI,在我看来,这似乎更自然,并且具有大部分好处。

      public class SawDI{
          public Saw CreateSaw(){
              return new HandSaw();
          }
      
          public SawMill CreateSawMill(){
              SawMill mill = new SawMill();
              mill.SetSaw(CreateSaw());
              return mill;
          }
      }
      
      // later on
      
      SawDI di = new SawDI();
      SawMill mill = di.CreateSawMill();
      

      这意味着我仍然集中耦合并拥有所有优点,而无需依赖更复杂的 DI 框架或 XML 配置文件。

      【讨论】:

      • 与使用控制容器的反转相比,这是很多重复输入工作
      • 您最终将不得不在 XML 配置文件中键入几乎与大多数情况下相同的内容。
      • 对于基于反射的 DI,这种方法对使用该系统的任何人来说都更加透明。
      • 对第 1 点的反应:这只是意味着您正在查看的 DI 框架很烂。有配置没有 xml 的 DI 框架。
      • 对第 2 点的反应:DI 的想法是代码的用户不必费心使用什么样的实现。如果你在乎,不要在那个地方使用 DI。紧耦合可以在正确的地方很好。
      【解决方案6】:

      大多数(如果不是全部)DI 容器/库还为您带来的一件事是可以拦截通过 DI 创建的所有实例的方法。

      【讨论】:

        【解决方案7】:

        不要忘记依赖注入的一个主要缺点:您失去了使用强大 Java IDE 的 Find Usages 轻松查找初始化位置的能力。如果您进行大量重构并希望避免测试代码比应用程序代码大 10 倍,这可能是一个非常严重的问题。

        【讨论】:

        • 是的。我也讨厌这个 - 但所有其他间接使用模式(例如“命令设计模式”、自动代理......)也遇到同样的问题。
        【解决方案8】:

        如果您对插入的类进行硬编码,则需要该类在编译时可用。使用配置文件,您可以在运行时更改使用的锯(在您的情况下)而无需重新编译,甚至可以使用从您刚刚放置在类路径中的新 jar 中获取的锯。是否值得额外的复杂性取决于您必须解决的任务。

        【讨论】:

        • 这与DI无关。
        • 所以,配置文件的目的是打败类型系统。
        • @Paco:你这么说很有趣,因为 DI (martinfowler.com/articles/injection.html) 的定义将其描述为一种“将配置与使用分开”的方式,这应该是有用的,因为你可以更改运行时使用的实现而不更改(和重新编译)客户端代码。
        【解决方案9】:

        最近,人们非常重视 DI 框架,以至于 DI 模式 被遗忘了。 DI 的原则为summarized by J. B. Rainsberger

        这很简单:通过要求合作者作为明确的依赖关系 构造函数中的参数。重复直到你推动了所有决定 关于在入口点创建哪些对象。当然,这只是 适用于服务(在 DDD 意义上)。完毕。

        正如您所注意到的,手动配置依赖项与使用框架并没有太大区别。使用构造函数注入,如下所示,您的代码示例将具有更少的样板文件,编译器将强制您提供所有必需的依赖项(并且您的 IDE 可能会为您键入它们)。

        SawMill sawMill = new SawMill(new HandSaw());
        sawMill.run();
        

        DI 框架可以减少创建工厂和将对象连接在一起的样板,但同时它也可以使找出每个依赖项的来源变得更加困难 - DI 框架的配置是多一层抽象来挖掘,你的 IDE 可能无法告诉你从哪里调用特定的构造函数。

        DI 框架的一个间接缺点是它们会使连接依赖项过于简单。当feel pain 无法再拥有大量依赖项时,您可能只是继续添加更多依赖项,而不是重新考虑应用程序的设计以减少类之间的耦合。手动连接依赖项 - 特别是在测试代码中 - 当测试设置变得更长并且编写单元测试变得更加困难时,当依赖项过多时更容易注意到。

        DI 框架的一些优势来自于它们对高级特性的支持,例如 AOP(例如 Spring 的 @Transactional)、作用域(尽管很多时候 scoping with plain code 就足够了)和可插入性(如果确实需要插件框架)。

        最近我做了一个手动 DI 与基于框架的 DI 的实验。进度和结果在Let's Code Dimdwarf episodes 42 through 47 中显示为截屏视频。有问题的项目有一个基于Guice 的插件系统,用于创建actors,然后我使用手动DI 重写了它,没有Guice。结果是一个更简单、更清晰的实现,并且只增加了一点样板。

        概要:首先尝试仅使用手动 DI(最好是构造函数注入)。如果有很多样板,请尝试重新考虑设计以减少依赖关系。如果 DI 框架的某些功能为您提供价值,请使用 DI 框架。

        【讨论】:

          【解决方案10】:

          了解 Spring 从根本上说是两件事情很重要,一件建立在另一件之上:

          1. 轻量级 DI/IoC 框架和实现该框架的类(例如 XML 应用程序上下文等);和
          2. 它是一个轻量级容器

          (2) 是大部分 Spring 代码。基本上选择一种 Java 技术,你可能会发现 Spring 有它的辅助类。这样您就可以使用 ActiveMQ、Sun One MQ 或其他任何东西,并将它们抽象为 Spring JmsTemplate,数据访问技术、Web 服务等也是如此。

          所有这些助手都使用 (1) 将它们连接在一起。

          【讨论】:

            【解决方案11】:

            使用依赖注入的最大好处之一是,在为该类创建单元测试时,它可以更轻松地提供该类的依赖项的模拟或存根。这使您可以独立地测试该类,而不依赖于它的协作者。

            在您的示例中,无法在实例化的类中模拟或存根 Saw 或 SawMill。如果 Saw 和 SawMill 是通过 setter 或构造函数设置的,那么您可以在运行单元测试时传入自己的 mock Saw 和 mock SawMill。

            【讨论】:

            • 如果在单元测试中没有另一种简单的方法来实现客户和协作者之间的分离,这只是一个好处。但事实是,这种简单的方式确实存在。有一些模拟工具可以做到这一点。
            • 确实,你是对的 Rogerio...我最近一直在使用你的 excellent JMockit 框架,它确实帮助我们为现有的代码库编写测试使用 DI。谢谢!
            【解决方案12】:

            Spring 可以帮助您理解甚至鼓励 DI 模型。但是,我不认为你必须有 Spring。

            您可以拥有 Java 中的配置文件,您可以对其进行调试、重构和执行代码分析。

            我建议你将这些 java 配置文件放在另一个包中,但它们在其他方面等同于 XML 配置文件。如果这很重要,您甚至可以动态加载这些文件。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-11-16
              • 1970-01-01
              • 1970-01-01
              • 2016-03-22
              • 2013-07-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多