理解DI框架的必要性

41

这可能是一个幼稚的问题。我正在学习Spring框架和依赖注入(DI)。虽然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框架不仅如此,但我在考虑依赖注入容器的需求。)

在第一个例子中,更改依赖项也是微不足道的:

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

这与https://dev59.com/8HVC5IYBdhLWcg3w-WOs非常相似。 - James McMahon
DI 的主要目的是简化应用程序配置并使测试更加容易/可能(DI 可以轻松替换依赖项)。 - Neil McGuigan
12个回答

17

依赖注入是一种退化的隐式参数传递形式,其目的本质上与解决所谓的配置问题相同:

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

依赖注入框架弥补了语言中缺乏隐式参数柯里化函数和方便的monads工具的不足。


15

我曾经也有同样的问题,这个问题被解答如下:
当然,你可以按照“然后你只需...”中的描述去做(我们称之为“类A”)。但是,这会将类A与HandSaw或者所有从SawMill类所需的依赖项耦合在一起。为什么A要与HandSaw耦合 - 或者更现实的情况是,为什么我的业务逻辑要与DAO层所需的JDBC连接实现耦合在一起呢?
那时我提出的解决方案是“将依赖项进一步移动” - 好的,现在我的视图与JDBC连接耦合,而我应该只处理HTML(或Swing,选择您所喜欢的)。

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

此外,您对于“plus:”存在一个误解(在您执行SawMill springSawMill = (SawMill)context.getBean("sawMill"); springSawMill.run();时) - 您不需要从上下文中获取sawMill bean - DI框架应该已经将sawMill bean注入到您的对象(类A)中。因此,您可以直接使用"sawMill.run()"而不关心它来自何处,谁初始化它以及如何初始化它。就您而言,它可以直接进入/dev/null、测试输出或真正的CnC引擎...重要的是 - 您不关心。您所关心的只是您的小小的类A,应该做它承诺要做的事情 - 激活一个锯木厂。


2
这个回答与我的理解相矛盾,与依赖注入模式的“官方”定义(http://martinfowler.com/articles/injection.html)不符。 只有在实现“Saw”抽象的实际类需要在运行时选择时,“A”类与“HandSaw”类的耦合才是一个问题。否则,在客户端代码中直接实例化Saw实现类是完全可以的。 依赖注入真正关注的是在那些确实需要的情况下“将配置与使用分离”,而不是默认情况下的所有情况。 - Rogério
6
只有在开始测试前才是真的。然后你会希望有一种方法将只进行断言的"TestSawImpl"放入A中,而不是一个需要木材堆、能源和经过认证的锯操作员(可能还要有医护人员待命)的真正的"HandSaw"。 - Ran Biron
4
不,这总是正确的。我可以使用模拟工具轻松地编写单元测试。不需要创建TestSawImpl,因为模拟工具可以用一行代码模拟任何Saw实现类,即使在编译时不知道具体的类。 - Rogério
引入另一层编织/动态类加载/其他技巧来使测试工作,将它们与它们应该模拟的真实世界进一步分开。虽然这样做是可能的,但我更支持简单的解决方案-在测试中使用setter并进行不同的设置。 - Ran Biron
1
好的,如果你更喜欢手动模拟,那对我来说也没问题。不过其他人发现使用模拟API是更简单的解决方案。 - Rogério
显示剩余2条评论

13

Spring有三个同等重要的特点:

  1. 依赖注入
  2. 面向切面编程
  3. 框架类库,用于帮助持久化、远程调用、Web MVC等。

如果将依赖注入与单个new调用进行比较,很难看出优势。在这种情况下,后者肯定会更简单,因为它只是一行代码。Spring的配置将始终增加代码行数,因此这不是一个获胜的论点。

当您可以将横切关注点(如事务)从类中分离出来并使用方面以声明方式设置时,情况就变得好多了。与单个“new”调用的比较不是Spring创建的原因。

也许使用Spring的最佳结果是其推荐的惯用语法使用接口、分层和良好的原则(如DRY)。这实际上只是Rod Johnson在他的咨询工作中使用的面向对象最佳实践的提炼。他发现,随着时间的推移,他构建的代码帮助他为客户交付更好的软件。他在《Expert 1:1 J2EE》中总结了自己的经验,并最终将代码开源为Spring。

我认为只有将这三个特点结合起来,才能获得Spring的全部价值。


6
“当然,这只是一个人为制造的例子。对于更复杂的对象关系,将其存储为XML文件可能比编程写入更有效,但肯定还有其他好处吧?”
“我认为将‘连线’放在配置文件中而不是手动在代码中进行操作更有意义,原因如下:”
1. “配置文件是外部的,与您的代码相互独立。”
2. “更改‘连线’(例如告诉您的‘锯木厂’使用不同的‘Saw’实例)可以直接在外部(XML)文件中进行修改,无需更改代码、重新编译、重新部署等操作。”
3. “当您有数十个类和几层注入时(例如:您有一个Web‘Controller’类,它获取一个包含业务逻辑的‘Service’类,该逻辑使用‘DAO’从数据库中获取‘Saw’,并将‘DataSource’注入其中等),手动连线是乏味的,并且需要几十行代码来完成连线。”
4. “这不太明显,但是通过将所有‘连线’都放在代码之外,我认为它有助于开发人员掌握依赖注入的核心思想,特别是面向接口编程而不是实现。通过手动连线,很容易陷入旧的方式。”

我认为前三个问题可能没有第四个那么明确。:) 外部配置增加了间接性(复杂性); 维护普通的 Java 代码比维护代码和 XML 配置更加愉快(虽然现代 IDE 对 Spring 有很好的支持); 这又回到了最初的问题... - Jonik
因此,依赖注入的本质是打败类型系统? - Apocalisp
我认为外部文件可以降低复杂性。不同于在代码中到处进行编码,信息都会被统一存储在一个地方。但这只是我的个人观点。 - matt b

5

通常我不太关心基于XML或反射的依赖注入,因为在我的使用案例中,它增加了不必要的复杂性。相反,我通常采用某种形式的手动依赖注入,对我来说更自然,并具有大部分的好处。

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配置文件。

这需要大量重复输入的工作,相比之下使用控制反转容器更为方便。 - Paco
大部分情况下,您将不得不在XML配置文件中输入与此相同的内容。 - Jasper Bekkers
在基于反射的依赖注入情况下,这种方法对于任何使用系统的人来说都更加透明。 - Jasper Bekkers
对第一点的反应:这只是意味着你正在考虑的依赖注入框架很糟糕。有些依赖注入框架可以在没有XML配置的情况下使用。 - Paco
对第二点的反应:DI 的理念是代码的使用者不必关心使用了什么样的实现。如果你在意,就不要在那个地方使用 DI。紧密耦合在正确的地方可能是有益的。 - Paco
显示剩余2条评论

3

几乎所有的 DI 容器/库都提供了拦截通过 DI 创建的所有实例方法的可能性。


3

不要忘记依赖注入的一个主要缺点:您失去了使用强大的Java IDE的“查找用途(Find Usages)”功能轻松找出某个东西从哪里初始化的能力。如果您需要经常进行重构并希望避免测试代码比应用程序代码多10倍,这可能是一个非常严重的问题。


是的。我也讨厌这个 - 但所有其他间接使用模式(如“命令设计模式”,自动代理...)也都面临同样的问题。 - Ran Biron

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

1
那与 DI 没有任何关系。 - Paco
1
因此,配置文件的目的是为了打破类型系统。 - Apocalisp
@Paco:有趣的是你这么说,因为DI(http://martinfowler.com/articles/injection.html)的定义恰恰描述它是一种“将配置与使用分离”的方式,这正是它有用的原因,因为你可以在运行时更改所使用的实现而不必更改(和重新编译)客户端代码。 - Rogério

2
最近非常强调 DI 框架,以至于 DI 模式被遗忘了。J.B. Rainsberger 总结 的 DI 原则是:
“很简单:通过在构造函数中要求协作者作为参数来明确依赖项。重复这个过程,直到您将有关创建哪些对象的所有决策都推入入口点。当然,这仅适用于服务(在 DDD 意义上)。完成。”
正如您所注意到的,手动配置依赖项与使用框架之间没有太大区别。使用构造函数注入,如下所示,您的代码示例将具有更少的样板文件,编译器将强制您提供所有必需的依赖项(并且您的 IDE 可能会为您键入它们)。
SawMill sawMill = new SawMill(new HandSaw());
sawMill.run();

一种 DI 框架可以减少创建工厂和将对象连接在一起的样板文件,但与此同时,它也可能使人难以找出每个依赖项来自哪里 - DI 框架的配置是一个更深层次的抽象层,需要挖掘,而你的 IDE 可能无法告诉你特定构造函数从哪里调用。DI 框架的间接劣势是,它们可能使得连接依赖项变得过于容易。当您无法再从许多依赖项中感受到疼痛时,您可能会继续添加更多的依赖项,而不是重新考虑应用程序的设计,以减少类之间的耦合度。手动连接依赖项 - 特别是在测试代码中 - 使你更容易注意到当测试设置变得更长时,你有太多依赖项,写单元测试也变得更加困难。
一些DI框架的优点源自于它们支持高级特性,比如AOP(例如Spring的@Transactional),作用域控制(尽管很多时候使用纯代码进行作用域控制就足够了)和可插拔性(如果真的需要一个插件框架的话)。最近我进行了手动DI和基于框架的DI的实验。进展和结果在“Let's Code Dimdwarf episodes 42 through 47”中以视频形式展示。该项目采用了基于Guice的插件系统来创建actors,之后我使用手动DI对其进行了重写,放弃了Guice。结果是实现方式更简单明了,只有少量模板代码。
简介:首先尝试仅使用手动 DI(最好是构造函数注入)。如果出现大量样板代码,请尝试重新思考设计以减少依赖关系。如果 DI 框架的某些功能对您提供了价值,请使用该框架。

1

理解Spring的基本概念非常重要,它基本上是由两个部分组成:

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

(2)是Spring代码的主体。基本上选择一个Java技术,你可能会发现Spring有它的辅助类。这样你就可以使用ActiveMQ、Sun One MQ或其他数据访问技术、Web服务等,并将它们抽象成为Spring JmsTemplate。所有这些辅助类都使用(1)来将它们连接在一起。


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