什么是依赖注入?

What is dependency injection?

提问人:AR. 提问时间:9/25/2008 最后编辑:Peter MortensenAR. 更新时间:4/9/2023 访问量:1165829

问:

已经发布了几个问题,其中包含有关依赖注入的具体问题,例如何时使用它以及它有哪些框架。然而

什么是依赖注入,何时/为什么应该或不应该使用它?

设计模式 语言无关的 依赖关系注入 术语

评论

44赞 Christian Payne 6/1/2009
我同意有关链接的评论。我能理解你可能想参考别人。但至少添加您链接它们的原因,以及是什么让这个链接比我使用谷歌获得的其他链接更好
0赞 Sean Reilly 8/28/2009
@AR:从技术上讲,依赖注入不是 IoC 的一种特殊形式。相反,IoC 是一种用于提供依赖注入的技术。其他技术可用于提供依赖注入(尽管 IoC 是唯一常用的技术),并且 IoC 也用于许多其他问题。
157赞 DOK 10/29/2010
关于链接,请记住,它们经常以一种或另一种方式消失。SO 答案中的死链接越来越多。所以,无论链接的文章有多好,如果你找不到它,它根本就不好。
0赞 Kevin S. 9/25/2008
请在此处查看我关于依赖注入的讨论。
0赞 ssmith 11/17/2015
依赖注入概述及其与其他 OOP 原则的关系: deviq.com/dependency-injection

答:

2161赞 wds 9/25/2008 #1

依赖注入是将依赖传递给其他对象框架依赖注入器)。

依赖注入使测试更容易。注入可以通过构造函数完成。

SomeClass()其构造函数如下:

public SomeClass() {
    myObject = Factory.getObject();
}

问题: 如果涉及磁盘访问或网络访问等复杂任务,则很难对 进行单元测试。程序员必须模拟并可能拦截工厂调用。myObjectSomeClass()myObject

替代解决方案

  • 作为参数传入构造函数myObject
public SomeClass (MyClass myObject) {
    this.myObject = myObject;
}

myObject可以直接通过,使测试更容易。

  • 一种常见的替代方法是定义一个不执行任何操作的构造函数。依赖注入可以通过 setter 完成。(h/t @MikeVella)。
  • Martin Fowler 记录了第三种选择(h/t @MarcDix),其中类显式地为程序员希望注入的依赖项实现接口。

在没有依赖关系注入的情况下,在单元测试中隔离组件是比较困难的。

2013 年,当我写下这个答案时,这是 Google 测试博客上的一个主要主题。对我来说,这仍然是最大的优势,因为程序员在运行时设计中并不总是需要额外的灵活性(例如,对于服务定位器或类似的模式)。程序员通常需要在测试期间隔离类。

评论

30赞 AR. 9/27/2008
承认 Ben Hoffstein 对 Martin Fowler 文章的引用是必要的,因为它指出了关于该主题的“必读”,我接受 wds 的回答,因为它实际上回答了 SO 上的问题。
138赞 Fuhrmanator 11/30/2012
+1 用于解释和动机:创建类依赖于其他人问题的对象。另一种说法是,DI 使类更具凝聚力(他们的职责更少)。
15赞 Mike Vella 8/8/2013
你说依赖项是“传递给构造函数”的,但据我了解,这并不严格正确。如果在对象实例化后将依赖项设置为属性,它仍然是依赖项注入,对吗?
2赞 wds 8/8/2013
@MikeVella 是的,没错。在大多数情况下,它没有真正的区别,尽管属性通常更灵活一些。我将稍微编辑文本以指出这一点。
3赞 Marc Dix 5/28/2016
到目前为止,我找到的最好的答案之一,因此我对改进它非常感兴趣。它缺少对依赖注入的第三种形式的描述:接口注入
295赞 Adam Ness 9/25/2008 #2

依赖注入是一种实践,其中对象的设计方式是它们从其他代码段接收对象的实例,而不是在内部构造它们。这意味着可以在不更改代码的情况下替换任何实现对象所需接口的对象,从而简化测试并改进解耦。

例如,考虑以下分支:

public class PersonService {
  public void addManager( Person employee, Person newManager ) { ... }
  public void removeManager( Person employee, Person oldManager ) { ... }
  public Group getGroupByManager( Person manager ) { ... }
}

public class GroupMembershipService() {
  public void addPersonToGroup( Person person, Group group ) { ... }
  public void removePersonFromGroup( Person person, Group group ) { ... }
} 

在此示例中,和的实现需要 的实例才能完成其工作。如果没有依赖注入,传统的方法是在构造函数中实例化 new,并在两个函数中使用该实例属性。但是,如果 的构造函数有多个它需要的东西,或者更糟糕的是,有一些初始化“setter”需要被调用,代码增长得相当快,现在不仅依赖于 ,还依赖于其他所有依赖的东西。此外,与的链接被硬编码为 这意味着您不能出于测试目的或在应用程序的不同部分使用策略模式而“虚拟”a。PersonService::addManagerPersonService::removeManagerGroupMembershipServiceGroupMembershipServicePersonServiceGroupMembershipServiceGroupMembershipServicePersonServiceGroupMembershipServiceGroupMembershipServiceGroupMembershipServicePersonServiceGroupMembershipService

使用依赖注入,而不是在 中实例化 ,您可以将其传递给构造函数,或者添加一个 Property(getter 和 setter)来设置它的本地实例。这意味着您不再需要担心如何创建一个,它只是接受它给出的那些,并与他们一起工作。这也意味着任何属于 的子类或实现接口的东西都可以“注入”到 中,并且不需要知道更改。GroupMembershipServicePersonServicePersonServicePersonServiceGroupMembershipServiceGroupMembershipServiceGroupMembershipServicePersonServicePersonService

评论

37赞 CodyBugstein 5/12/2014
如果您在使用 DI 后能给出相同的代码示例,那就太好了
2赞 DefinedRisk 4/9/2021
“这也意味着,任何作为 GroupMembershipService 的子类或实现 GroupMembershipService 接口的内容都可以”注入“到 PersonService 中,而 PersonService 不需要知道更改。”这对我来说是一个非常有用的方法——谢谢!
2790赞 Thiago Arrais 9/27/2008 #3

到目前为止,我发现的最好的定义是詹姆斯·肖尔(James Shore)的定义

“依赖注入”是 25 美元 5 美分概念的术语。[...] 依赖注入意味着给一个 对象其实例变量。[...].

马丁·福勒(Martin Fowler)的一篇文章也可能被证明是有用的。

依赖注入基本上是提供对象所需的对象(其依赖关系),而不是让它自己构造它们。这是一种非常有用的测试技术,因为它允许模拟或存根依赖项。

依赖项可以通过多种方式(例如构造函数注入或 setter 注入)注入到对象中。人们甚至可以使用专门的依赖注入框架(例如Spring)来做到这一点,但它们肯定不是必需的。你不需要这些框架来注入依赖关系。显式实例化和传递对象(依赖项)与框架注入一样好。

评论

60赞 Matt 11/26/2015
我喜欢 James 文章的解释,尤其是结尾:“尽管如此,你还是不得不惊叹于任何采用三个概念('TripPlanner'、'CabAgency'和'AirlineAgency')的方法,将它们变成九个以上的类,然后在编写一行应用程序逻辑之前添加数十行胶水代码和配置 XML。这就是我经常看到的(可悲的是)——依赖注入(正如他所解释的那样,这本身是好的)被滥用来使本来可以更容易完成的事情过于复杂——最终编写了“支持”代码......
6赞 dzieciou 12/5/2015
回复:“显式实例化和传递对象(依赖项)与框架注入一样好。那么,为什么人们会这样做呢?
27赞 Thiago Arrais 1/12/2016
出于与每个框架被编写(或至少应该被编写)相同的原因:因为一旦你达到一定的复杂性,就需要编写大量重复/样板代码。问题在于,很多时候人们会去寻找一个框架,即使它不是严格需要的。
28赞 Christine 10/1/2016
25 美分概念的 5 美元期限已经死了。这是一篇对我有帮助的好文章:codeproject.com/Articles/615139/......
2赞 M H 10/21/2020
当您使用 DI 容器时,为您构建对象图也@dzieciou很好,并且能够在一个地方将一个实现换成另一个实现也很好。一般来说,对于愚蠢、简单的东西,我可能会传递依赖项,但在大多数框架中使用 DI 容器非常容易,通常也很容易做到这一点。
200赞 zby 1/7/2011 #4

公认的答案是一个很好的答案 - 但我想补充一点,DI 非常像经典的避免代码中的硬编码常量。

当你使用一些常量(如数据库名称)时,你可以快速地将它从代码内部移动到某个配置文件,并将包含该值的变量传递到需要它的位置。这样做的原因是,这些常量通常比代码的其余部分更频繁地更改。例如,如果要在测试数据库中测试代码。

DI 类似于面向对象编程领域中的这一点。那里的值而不是常量文字是整个对象 - 但从类代码中移出创建它们的代码的原因相似 - 对象比使用它们的代码更频繁地更改。需要进行此类更改的一个重要情况是测试。

评论

22赞 Chethan 3/30/2014
+1 “对象的变化比使用它们的代码更频繁”。为了概括化,在通量点添加间接。根据通量点的不同,间接名称不同!!
830赞 gtiwari333 5/22/2011 #5

松耦合方面,我发现了这个有趣的例子:

来源:了解依赖注入

任何应用程序都由许多对象组成,这些对象相互协作以执行一些有用的操作。传统上,每个对象负责获取自己对与之协作的依赖对象(依赖项)的引用。这会导致高度耦合的类和难以测试的代码。

例如,考虑一个对象。Car

A依靠车轮、发动机、燃料、电池等来运行。传统上,我们定义此类依赖对象的品牌以及对象的定义。CarCar

无依赖注入 (DI):

class Car{
  private Wheel wh = new NepaliRubberWheel();
  private Battery bt = new ExcideBattery();

  //The rest
}

在这里,对象负责创建依赖对象。Car

如果我们想在最初的穿刺后改变其依赖对象的类型——比如说——怎么办? 我们需要用它的新依赖关系重新创建 Car 对象,但只有制造商才能做到这一点。WheelNepaliRubberWheel()ChineseRubberWheel()Car

那么依赖注入为我们做了什么......?

使用依赖关系注入时,对象在运行时而不是在编译时(汽车制造时间)被赋予其依赖关系。 这样我们现在可以随时更改。在这里,可以在运行时注入 ()。WheeldependencywheelCar

使用依赖注入后:

在这里,我们在运行时注入依赖项(Wheel 和 Battery)。因此,术语:依赖注入。我们通常依靠 Spring、Guice、Weld 等 DI 框架来创建依赖项并在需要时注入。

class Car{
  private Wheel wh; // Inject an Instance of Wheel (dependency of car) at runtime
  private Battery bt; // Inject an Instance of Battery (dependency of car) at runtime
  Car(Wheel wh,Battery bt) {
      this.wh = wh;
      this.bt = bt;
  }
  //Or we can have setters
  void setWheel(Wheel wh) {
      this.wh = wh;
  }
}

优点是:

  • 解耦对象的创建(换句话说,将用法与对象的创建分开)
  • 能够替换依赖项(例如:车轮、电池),而无需更改使用它的类(汽车)
  • 提倡“代码对接口,而不是实现”的原则
  • 能够在测试期间创建和使用 mock 依赖(如果我们想在测试期间使用 Mock of Wheel 而不是真实实例.. 我们可以创建 Mock Wheel 对象并让 DI 框架注入到 Car)

评论

29赞 JeliBeanMachine 5/29/2014
我的理解是,我们可以在需要时注入该对象,从而消除第一个对象对它的依赖,而不是将新对象实例化为另一个对象的一部分。是吗?
0赞 Ali Nem 11/12/2016
我在这里用咖啡店的例子描述了这一点:digigene.com/design-patterns/dependency-injection-coffeeshop
14赞 Jeb50 8/10/2017
真的很喜欢这个类比,因为它是使用简单类比的简单英语。假设我是丰田,已经花费了太多的财力和人力来制造汽车从设计到下线,如果已经有信誉良好的轮胎生产商,我为什么要从头开始做一个轮胎制造部门,即轮胎?我没有。我所要做的就是从他们那里购买(通过参数注入),安装并哇啦!所以,回到编程,假设一个 C# 项目需要使用一个现有的库/类,有两种方法可以运行/调试,1-add 引用整个项目new
0赞 Jeb50 8/10/2017
(续),..外部库/类,或从 DLL 添加它。除非我们必须查看这个外部类内部的内容,否则将其添加为 DLL 是一种更简单的方法。所以选项 1 是它,选项 2 是作为参数传入。可能不准确,但简单愚蠢,易于理解。new
3赞 Mikael Ohlson 1/3/2018
@JeliBeanMachine(对不起,对评论的回复太晚了..) 这并不是说我们删除了第一个对象对 wheel 对象或电池对象的依赖关系,而是我们向它传递了依赖关系,以便我们可以更改依赖关系的实例或实现。之前:Car 对 NepaliRubberWheel 有硬编码依赖关系。之后:Car 对 Wheel 实例具有注入的依赖关系。
136赞 Olivier Liechti 10/22/2012 #6

让我们想象一下,你想去钓鱼:

  • 如果没有依赖注入,您需要自己处理所有事情。你需要找到一艘船,买一根钓鱼竿,寻找诱饵等。当然,这是可能的,但它给你带来了很多责任。用软件术语来说,这意味着您必须对所有这些东西执行查找。

  • 通过依赖注入,其他人负责所有准备工作,并为您提供所需的设备。您将收到(“注射”)船、钓鱼竿和诱饵 - 都可以使用。

评论

65赞 jscs 6/13/2013
另一方面,想象一下,你雇了一个水管工来重做你的浴室,然后他说,“太好了,这是我需要你为我买的工具和材料的清单”。这不应该是水管工的工作吗?
0赞 10/25/2013
所以有人需要照顾一个它不知道的人。但仍然决定收集船、棍子和诱饵的清单——尽管可以使用。
29赞 sara 2/12/2016
@JoshCaswell 不,那是水管工的工作。作为客户,您需要完成管道。为此,您需要一个水管工。水管工需要它的工具。为了获得这些,它由管道公司配备。作为客户,您不想知道水管工到底在做什么或需要什么。作为一名水管工,你知道你需要什么,但你只想做好你的工作,而不是得到一切。作为水管工雇主,您有责任在将水管工送到人们家中之前为他们的水管工配备他们需要的东西。
1赞 cHao 4/7/2017
@KingOfAllTrades:当然,在某些时候,你必须有人雇用和装备水管工,否则你就没有水管工。但是你没有让客户这样做。客户只是要求一个水管工,并得到一个已经配备了他完成工作所需的水管工。使用 DI,您最终仍然有一些代码来满足依赖关系。但是你把它从实际工作的代码中分离出来。如果你把它发挥到极致,你的对象只是让它们的依赖关系为人所知,而对象图的构建发生在外部,通常是在初始化代码中。
1赞 Olivier Liechti 7/19/2022
@ThomasRones:差不多。实际上,有不同的方法可以将依赖项传递给对象。一种方法是将它们作为参数传递到构造函数中。另一种方法是使用 setter 方法(或某种注释)。有些人强烈建议使用基于构造函数的 DI(参见 odrotbohm.de/2013/11/why-field-injection-is-evil)。
69赞 JaneGoodall 5/2/2013 #7

“依赖注入”不仅仅意味着使用参数化构造函数和公共 setter 吗?

James Shore 的文章展示了以下示例以供比较

没有依赖注入的构造函数:

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example() { 
    myDatabase = new DatabaseThingie(); 
  } 

  public void doStuff() { 
    ... 
    myDatabase.getData(); 
    ... 
  } 
} 

具有依赖注入的构造函数:

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example(DatabaseThingie useThisDatabaseInstead) { 
    myDatabase = useThisDatabaseInstead; 
  }

  public void doStuff() { 
    ... 
    myDatabase.getData(); 
    ... 
  } 
}

评论

0赞 Matt Wilko 11/21/2013
当然,在 DI 版本中,您不想在 no 参数构造函数中初始化 myDatabase 对象吗?似乎没有意义,如果您尝试在不调用重载构造函数的情况下调用 DoStuff,则会引发异常?
0赞 JaneGoodall 11/23/2013
仅当不生成有效的 myDatabase 实例时。new DatabaseThingie()
6赞 TastyCode 5/19/2013 #8

摘自《扎实的 Java 开发人员:Java 7 和多语言编程的重要技术》一书

DI 是 IoC 的一种特殊形式,通过它查找依赖项的过程是 不受当前执行代码的直接控制。

28赞 8 revs, 3 users 97%uvsmtid #9

依赖注入 (DI) 的全部意义在于保持应用程序源代码的干净和稳定

  • 清理依赖项初始化代码
  • 无论使用何种依赖关系,都保持稳定

实际上,每种设计模式都分离了关注点,以使将来的更改影响最小的文件。

DI 的特定域是依赖项配置和初始化的委派。

示例:带有 shell 脚本的 DI

如果您偶尔在 Java 之外工作,回想一下它在许多脚本语言(Shell、Tcl 等,甚至在 Python 中被误用)中的频繁使用方式。sourceimport

考虑简单的脚本:dependent.sh

#!/bin/sh
# Dependent
touch         "one.txt" "two.txt"
archive_files "one.txt" "two.txt"

脚本是依赖的:它不会自行成功执行(未定义)。archive_files

您可以在实现脚本中定义(在本例中使用):archive_filesarchive_files_zip.shzip

#!/bin/sh
# Dependency
function archive_files {
    zip files.zip "$@"
}

您不是直接在依赖脚本中使用 -ing 实现脚本,而是使用包装两个“组件”的“容器”:sourceinjector.sh

#!/bin/sh 
# Injector
source ./archive_files_zip.sh
source ./dependent.sh

依赖刚刚注入依赖脚本中。archive_files

您可以注入依赖项,该依赖项使用 或 实现。archive_filestarxz

示例:删除 DI

如果脚本直接使用依赖项,则该方法将称为依赖项查找(与依赖项注入相反):dependent.sh

#!/bin/sh
# Dependent

# dependency look-up
source ./archive_files_zip.sh

touch         "one.txt" "two.txt"
archive_files "one.txt" "two.txt"

现在的问题是,依赖的“组件”必须自己执行初始化。

“组件”的源代码既不干净也不稳定,因为依赖项初始化的每次更改都需要“组件”的源代码文件的新版本。

最后的话

DI 不像 Java 框架那样受到广泛重视和普及。

但这是一种通用的方法,用于拆分以下问题:

  • 应用程序开发单一源代码发布生命周期)
  • 应用程序部署具有独立生命周期的多个目标环境)

仅将配置与依赖项查找一起使用无济于事,因为每个依赖项的配置参数数量(例如,新的身份验证类型)以及支持的依赖项类型(例如,新的数据库类型)的数量可能会发生变化。

评论

0赞 David 9/8/2016
我会添加完成特定类(测试)的能力,而不必完成其依赖项,作为 DI 的目的。
10赞 user2537701 10/25/2013 #10

我想既然每个人都为DI写过文章,让我问几个问题。

  1. 当你有一个DI的配置,其中所有的实际实现(不是接口)将被注入到一个类中(例如,为控制器提供服务),为什么这不是某种硬编码?
  2. 如果我想在运行时更改对象怎么办?例如,我的配置已经说当我实例化 MyController 时,将 FileLogger 作为 ILogger 注入。但是我可能想注入 DatabaseLogger。
  3. 每次我想更改我的 AClass 需要的对象时,我现在都需要查看两个位置 - 类本身和配置文件。这如何让生活更轻松?
  4. 如果没有注入 AClass 的 Aproperty,模拟它更难吗?
  5. 回到第一个问题。如果使用 new object() 不好,为什么我们注入实现而不是接口?我想你们很多人都在说我们实际上是在注入接口,但配置让你指定该接口的实现。不在运行时.它在编译时被硬编码。

这是基于N发布的@Adam答案。

为什么 PersonService 不再需要担心 GroupMembershipService?您刚才提到 GroupMembership 有多个它所依赖的东西(对象/属性)。如果 PService 中需要 GMService,则可以将其作为属性。无论您是否注入了它,您都可以模拟它。我唯一希望注入它的时候是 GMService 是否有更具体的子类,直到运行时你才会知道。然后,您需要注入子类。或者,如果您想将其用作单例或原型。老实说,配置文件的所有内容都硬编码,直到它将在编译时注入类型(接口)的子类。

编辑

何塞·玛丽亚·阿兰兹(Jose Maria Arranz)对DI的精彩评论

DI 通过消除确定依赖关系方向和编写任何粘附代码的需要来增加内聚力。

假。依赖项的方向是 XML 形式或作为注释,您的依赖项是以 XML 代码和注释的形式编写的。XML 和注释是源代码。

DI 通过使所有组件模块化(即可更换)并具有明确定义的接口来减少耦合。

假。您不需要 DI 框架来构建基于接口的模块化代码。

关于可替换:使用非常简单的 .properties 存档和 Class.forName,您可以定义哪些类可以更改。如果可以更改代码的任何类,Java 不适合您,请使用脚本语言。顺便说一句:如果不重新编译,就无法更改注释。

在我看来,DI 框架的唯一原因只有一个:减少样板。有了完善的工厂系统,您可以像您首选的 DI 框架一样,更可控、更可预测,DI 框架承诺减少代码(XML 和注释也是源代码)。问题在于,这种样板减少在非常简单的情况下是真实的(每个类一个实例和类似情况),有时在现实世界中,选择适当的服务对象并不像将类映射到单例对象那样容易。

4赞 Waqas Ahmed 11/12/2013 #11

简单来说,依赖注入 (DI) 是消除不同对象之间的依赖关系或紧密耦合的方法。依赖注入为每个对象提供内聚行为。

DI 是 Spring 的 IOC 原则的实现,它说“不要打电话给我们,我们会打电话给你”。使用依赖注入程序员不需要使用 new 关键字创建对象。

对象一旦加载到 Spring 容器中,然后我们通过使用 getBean(String beanName) 方法从 Spring 容器中获取这些对象来在需要时重用它们。

14赞 Piyush Deshpande 1/7/2014 #12

这意味着对象应该只具有完成其工作所需的尽可能多的依赖项,并且依赖项应该很少。此外,如果可能的话,对象的依赖关系应该在接口上,而不是在“具体”对象上。(具体对象是使用关键字 new 创建的任何对象。松耦合提高了可重用性,更易于维护,并允许您轻松地提供“模拟”对象来代替昂贵的服务。

“依赖注入”(DI)也称为“控制反转”(IoC),可以用作鼓励这种松耦合的技术。

实现 DI 有两种主要方法:

  1. 构造函数注入
  2. 孵化器注入

构造函数注入

它是将对象依赖项传递给其构造函数的技术。

请注意,构造函数接受的是接口,而不是具体对象。另请注意,如果 orderDao 参数为 null,则会引发异常。这强调了接收有效依赖关系的重要性。在我看来,构造函数注入是赋予对象依赖项的首选机制。开发人员在调用对象时很清楚,需要将哪些依赖项赋予“Person”对象才能正确执行。

孵化器注入

但请考虑以下示例...假设您有一个包含十个没有依赖关系的方法的类,但您正在添加一个依赖于 IDAO 的新方法。您可以将构造函数更改为使用构造函数注入,但这可能会迫使您到处更改所有构造函数调用。或者,您可以只添加一个接受依赖项的新构造函数,但是开发人员如何轻松知道何时使用一个构造函数而不是另一个构造函数。最后,如果创建依赖项的成本非常高,为什么还要创建它并将其传递给构造函数,而它可能很少使用?“Setter Injection”是另一种可用于此类情况的 DI 技术。

Setter 注入不会强制将依赖项传递给构造函数。相反,依赖项被设置到需要的对象公开的公共属性上。如前所述,这样做的主要动机包括:

  1. 支持依赖注入,而无需修改旧类的构造函数。
  2. 允许尽可能晚地创建昂贵的资源或服务,并且仅在需要时创建。

下面是上述代码的示例:

public class Person {
    public Person() {}

    public IDAO Address {
        set { addressdao = value; }
        get {
            if (addressdao == null)
              throw new MemberAccessException("addressdao" +
                             " has not been initialized");
            return addressdao;
        }
    }

    public Address GetAddress() {
       // ... code that uses the addressdao object
       // to fetch address details from the datasource ...
    }

    // Should not be called directly;
    // use the public property instead
    private IDAO addressdao;

评论

3赞 Jay Sullivan 1/20/2014
我认为你的第一段偏离了问题,根本不是 DI 的定义(即,你试图定义 SOLID,而不是 DI)。从技术上讲,即使你有 100 个依赖项,你仍然可以使用依赖项注入。类似地,可以注入具体的依赖关系——它仍然是依赖关系注入。
6赞 Volksman 4/2/2014 #13

依赖注入是通常称为“依赖混淆”要求的一种可能解决方案。依赖性混淆是一种将“明显”性质从向需要它的类提供依赖关系的过程中剔除的方法,因此以某种方式混淆了向所述类提供所述依赖关系的过程。这不一定是坏事。事实上,通过混淆向类提供依赖关系的方式,类外部的东西负责创建依赖关系,这意味着在各种情况下,可以向类提供依赖关系的不同实现,而无需对类进行任何更改。这非常适合在生产模式和测试模式之间切换(例如,使用“模拟”服务依赖项)。

不幸的是,糟糕的部分是,有些人认为你需要一个专门的框架来做依赖混淆,如果你选择不使用特定的框架来做,你在某种程度上是一个“低级”程序员。许多人认为,另一个极其令人不安的神话是,依赖注入是实现依赖混淆的唯一方法。这在历史上显然是 100% 错误的,但你很难说服一些人,你的依赖关系混淆要求有依赖注入的替代方案。

多年来,程序员已经了解依赖性混淆要求,并且在依赖性注入构思之前和之后,许多替代解决方案都得到了发展。有工厂模式,但也有许多使用 ThreadLocal 的选项,其中不需要注入到特定实例 - 依赖项被有效地注入到线程中,这样做的好处是使对象可用(通过方便的静态 getter 方法)到任何需要它的类,而不必向需要它的类添加注释,并设置复杂的 XML“胶水”来实现它。当持久性需要依赖项(JPA/JDO 或其他)时,它允许你更轻松地实现“跨父持久性”,并且使用完全由 POJO 组成的领域模型和业务模型类(即没有特定于框架/锁定在注释中)。

3赞 mohit sarsar 5/28/2014 #14

依赖注入是Spring Framework相关概念的核心,虽然创建任何项目的框架,但Spring都可能起到至关重要的作用,而这里依赖注入就派上用场了。

实际上,假设在 java 中创建了两个不同的类作为类 A 和类 B,并且无论类 B 中可用的函数是什么,您都想在类 A 中使用,因此此时可以使用依赖注入。 你可以将一个类的对象放入另一个类中,同样,你可以将整个类注入另一个类以使其可访问。 通过这种方式,可以克服依赖性。

依赖注入只是将两个类粘合在一起,同时将它们分开。

11赞 Alex 11/12/2014 #15

我知道已经有很多答案,但我发现这很有帮助:http://tutorials.jenkov.com/dependency-injection/index.html

无依赖关系:

public class MyDao {

  protected DataSource dataSource = new DataSourceImpl(
    "driver", "url", "user", "password");

  //data access methods...
  public Person readPerson(int primaryKey) {...}     
}

屬地:

public class MyDao {

  protected DataSource dataSource = null;

  public MyDao(String driver, String url, String user, String password) {
    this.dataSource = new DataSourceImpl(driver, url, user, password);
  }

  //data access methods...
  public Person readPerson(int primaryKey) {...}
}

请注意实例化是如何移动到构造函数中的。构造函数采用四个参数,这些参数是 所需的四个值。尽管类仍然依赖于这四个值,但它本身不再满足这些依赖关系。它们由创建实例的任何类提供。DataSourceImplDataSourceImplMyDaoMyDao

评论

2赞 PmanAce 5/1/2016
DI 不会通过已经构造的 DataSourceImp 接口传递您吗?
38赞 StuartLC 4/29/2015 #16

什么是依赖注入 (DI)?

正如其他人所说,依赖注入(DI)消除了直接创建和管理我们感兴趣的类(消费者类)所依赖的其他对象实例(在UML意义上)的生命周期的责任。相反,这些实例被传递给我们的使用者类,通常作为构造函数参数或通过属性设置器(依赖对象实例化和传递给使用者类的管理通常由控制反转 (IoC) 容器执行,但这是另一个主题)。

DI、DIP 和固体

具体来说,在 Robert C Martin 的面向对象设计的 SOLID 原则范式中,是依赖反转原则 (DIP) 的可能实现之一。DIP 是 SOLID 口头禅的 D - 其他 DIP 实现包括服务定位器和插件模式。DI

DIP 的目标是解耦类之间紧密的、具体的依赖关系,而是通过抽象来松开耦合,这可以通过 或 来实现,具体取决于所使用的语言和方法。interfaceabstract classpure virtual class

如果没有 DIP,我们的代码(我称之为“消费类”)直接耦合到具体的依赖关系,并且通常还承担着知道如何获取和管理这种依赖关系的实例的责任,即在概念上:

"I need to create/use a Foo and invoke method `GetBar()`"

而在应用 DIP 之后,要求被放宽,并且已经消除了获取和管理依赖关系生命周期的问题:Foo

"I need to invoke something which offers `GetBar()`"

为什么要使用 DIP(和 DI)?

以这种方式解耦类之间的依赖关系,可以很容易地将这些依赖类替换为其他实现,这些实现也满足抽象的先决条件(例如,依赖关系可以与同一接口的另一个实现进行切换)。此外,正如其他人所提到的,通过 DIP 解耦类的最常见原因可能是允许单独测试消费类,因为这些相同的依赖项现在可以被存根和/或模拟。

DI 的一个结果是依赖对象实例的生命周期管理不再由消费类控制,因为依赖对象现在被传递到消费类中(通过构造函数或 setter 注入)。

这可以通过不同的方式查看:

  • 如果需要保留消费类对依赖关系的生命周期控制,可以通过向使用者类注入用于创建依赖类实例的(抽象)工厂来重新建立控制。使用者将能够根据需要通过工厂获取实例,并在完成后处理这些实例。Create
  • 或者,可以将依赖项实例的生命周期控制交给 IoC 容器(下文将对此进行更多介绍)。

何时使用 DI?

  • 如果可能需要用依赖项代替等效的实现,
  • 任何时候,当你需要对一个类的方法进行单元测试时,它就会独立于它的依赖关系,
  • 依赖项生命周期的不确定性可能需要进行实验(例如,嘿,线程安全吗 - 如果我们将其设置为单例并将相同的实例注入所有消费者会怎样?MyDepClass

下面是一个简单的 C# 实现。给定以下 Consumeing 类:

public class MyLogger
{
   public void LogRecord(string somethingToLog)
   {
      Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
   }
}

虽然看似无害,但它对另外两个类有两个依赖关系,并且 ,这不仅限制了日志记录输出选项(如果没有人监视,则记录到控制台将毫无价值),而且更糟糕的是,鉴于对非确定性系统时钟的依赖性,很难自动测试。staticSystem.DateTimeSystem.Console

但是,我们可以应用于此类,方法是将时间戳作为依赖项的问题抽象出来,并且仅耦合到一个简单的接口:DIPMyLogger

public interface IClock
{
    DateTime Now { get; }
}

我们还可以放松对抽象的依赖,例如 .依赖注入通常实现为注入(将抽象作为参数传递给依赖项到使用类的构造函数)或(通过 setter 或 .Net 属性传递依赖项)。构造函数注入是首选,因为这可以保证类在构造后处于正确的状态,并允许将内部依赖项字段标记为 (C#) 或 (Java)。因此,在上面的例子中使用构造函数注入,这给我们留下了:ConsoleTextWriterconstructorSetter InjectionsetXyz(){set;}readonlyfinal

public class MyLogger : ILogger // Others will depend on our logger.
{
    private readonly TextWriter _output;
    private readonly IClock _clock;

    // Dependencies are injected through the constructor
    public MyLogger(TextWriter stream, IClock clock)
    {
        _output = stream;
        _clock = clock;
    }

    public void LogRecord(string somethingToLog)
    {
        // We can now use our dependencies through the abstraction 
        // and without knowledge of the lifespans of the dependencies
        _output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
    }
}

(需要提供具体,当然可以恢复到 ,并且这两个依赖项需要由 IoC 容器通过构造函数注入提供)ClockDateTime.Now

可以构建一个自动化的单元测试,它明确地证明了我们的记录器工作正常,因为我们现在可以控制依赖项 - 时间,我们可以监视写入的输出:

[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
    // Arrange
    var mockClock = new Mock<IClock>();
    mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
    var fakeConsole = new StringWriter();

    // Act
    new MyLogger(fakeConsole, mockClock.Object)
        .LogRecord("Foo");

    // Assert
    Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}

后续步骤

依赖注入总是与控制反转容器 (IoC) 相关联,以注入(提供)具体的依赖实例,并管理生命周期实例。在配置/引导过程中,容器允许定义以下内容:IoC

  • 每个抽象和配置的具体实现之间的映射(例如,“每当消费者请求 IBar 时,返回一个 ConcreteBar 实例”)
  • 可以为每个依赖的生命周期管理设置策略,例如为每个消费者实例创建一个新对象,在所有消费者之间共享一个单例依赖实例,仅在同一个线程上共享同一个依赖实例等。
  • 在 .Net 中,IoC 容器知道协议,并将根据配置的生命周期管理承担依赖项的责任。IDisposableDisposing

通常,一旦 IoC 容器被配置/引导,它们就会在后台无缝运行,使编码人员能够专注于手头的代码,而不必担心依赖关系。

DI 友好代码的关键是避免类的静态耦合,并且不使用 new() 来创建依赖项

如上例所示,依赖关系的解耦确实需要一些设计工作,对于开发人员来说,需要一种范式转变,以打破直接处理依赖关系的习惯,而是信任容器来管理依赖关系。new

但好处很多,尤其是在彻底测试您感兴趣的类别的能力方面。

注意:POCO/POJO/序列化DTO/实体图/匿名JSON投影等的创建/映射/投影(通过)-即“仅数据”类或记录--使用或返回的方法不被视为依赖关系(在UML意义上),不受DI的约束。用来投影这些就好了。new ..()new

评论

1赞 Ricardo Rivaldo 10/1/2015
问题是 DIP != DI。DIP 是关于将抽象与实现解耦: A. 高级模块不应依赖于低级模块。两者都应该依赖于抽象。B. 抽象不应依赖于细节。细节应取决于抽象。DI 是一种将对象创建与对象使用分离的方法。
0赞 StuartLC 3/16/2017
是的,在我的第 2 段中明确说明了这种区别,“DI 是 DIP 的可能实现之一”,在 Uncle Bob 的 SOLID 范式中。我在之前的一篇文章中也明确了这一点。
8赞 Phil Goetz 6/3/2015 #17

流行的答案是无济于事的,因为它们以一种无用的方式定义依赖注入。让我们同意,我们所说的“依赖”是指我们的对象 X 需要的一些预先存在的其他对象。但是当我们说我们说时,我们并没有说我们在做“依赖注入”

$foo = Foo->new($bar);

我们只需将其传递参数调用到构造函数中。自从构造函数被发明以来,我们就经常这样做。

“依赖注入”被认为是一种“控制反转”,这意味着从调用方中取出了一些逻辑。当调用方传入参数时,情况并非如此,因此如果是 DI,则 DI 不会意味着控制反转。

DI 意味着调用方和构造函数之间有一个中间级别,用于管理依赖项。Makefile 是依赖注入的一个简单示例。“调用者”是在命令行上键入“make bar”的人,“构造函数”是编译器。Makefile 指定 bar 依赖于 foo,并且它执行

gcc -c foo.cpp; gcc -c bar.cpp

在做之前

gcc foo.o bar.o -o bar

键入“make bar”的人不需要知道 bar 依赖于 foo。依赖关系在“make bar”和 gcc 之间注入。

中间级别的主要目的不仅仅是将依赖项传递给构造函数,而是在一个地方列出所有依赖项,并将它们隐藏起来(而不是让编码人员提供它们)。

通常,中间级别为构造的对象提供工厂,这些对象必须提供每个请求的对象类型必须满足的角色。那是因为通过隐藏构造细节的中间级别,你已经招致了工厂施加的抽象惩罚,所以你还不如使用工厂。

4赞 BERGUIGA Mohamed Amine 8/28/2015 #18

摘自 Book Apress.Spring.Persistence.with.Hibernate.Oct.2010

依赖注入的目的是解耦 从应用程序业务中解析外部软件组件 逻辑。没有依赖注入,组件的细节如何 访问所需的服务可能会与组件的 法典。这不仅增加了出错的可能性,还增加了代码 膨胀,并放大维护复杂性;它耦合组件 更紧密地结合在一起,使得在以下情况下难以修改依赖项 重构或测试。

4赞 hariprasad 9/20/2015 #19

依赖注入 (DI) 是设计模式中的一种,它使用 OOP 的基本特性 - 一个对象与另一个对象的关系。虽然继承继承一个对象以执行更复杂和更具体的另一个对象,但关系或关联只是使用属性从一个对象创建指向另一个对象的指针。DI 的强大功能与 OOP 的其他功能相结合,接口和隐藏代码也是如此。 假设,我们在图书馆里有一个客户(订阅者),为了简单起见,它只能借一本书。

书的界面:

package com.deepam.hidden;

public interface BookInterface {

public BookInterface setHeight(int height);
public BookInterface setPages(int pages);   
public int getHeight();
public int getPages();  

public String toString();
}

接下来,我们可以有很多种类的书;其中一种类型是虚构的:

package com.deepam.hidden;

public class FictionBook implements BookInterface {
int height = 0; // height in cm
int pages = 0; // number of pages

/** constructor */
public FictionBook() {
    // TODO Auto-generated constructor stub
}

@Override
public FictionBook setHeight(int height) {
  this.height = height;
  return this;
}

@Override
public FictionBook setPages(int pages) {
  this.pages = pages;
  return this;      
}

@Override
public int getHeight() {
    // TODO Auto-generated method stub
    return height;
}

@Override
public int getPages() {
    // TODO Auto-generated method stub
    return pages;
}

@Override
public String toString(){
    return ("height: " + height + ", " + "pages: " + pages);
}
}

现在,订阅者可以与该书相关联:

package com.deepam.hidden;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Subscriber {
BookInterface book;

/** constructor*/
public Subscriber() {
    // TODO Auto-generated constructor stub
}

// injection I
public void setBook(BookInterface book) {
    this.book = book;
}

// injection II
public BookInterface setBook(String bookName) {
    try {
        Class<?> cl = Class.forName(bookName);
        Constructor<?> constructor = cl.getConstructor(); // use it for parameters in constructor
        BookInterface book = (BookInterface) constructor.newInstance();
        //book = (BookInterface) Class.forName(bookName).newInstance();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    return book;
}

public BookInterface getBook() {
  return book;
}

public static void main(String[] args) {

}

}

所有这三个类都可以隐藏起来,以便实现它自己的实现。现在我们可以将以下代码用于 DI:

package com.deepam.implement;

import com.deepam.hidden.Subscriber;
import com.deepam.hidden.FictionBook;

public class CallHiddenImplBook {

public CallHiddenImplBook() {
    // TODO Auto-generated constructor stub
}

public void doIt() {
    Subscriber ab = new Subscriber();

    // injection I
    FictionBook bookI = new FictionBook();
    bookI.setHeight(30); // cm
    bookI.setPages(250);
    ab.setBook(bookI); // inject
    System.out.println("injection I " + ab.getBook().toString());

    // injection II
    FictionBook bookII = ((FictionBook) ab.setBook("com.deepam.hidden.FictionBook")).setHeight(5).setPages(108); // inject and set
    System.out.println("injection II " + ab.getBook().toString());      
}

public static void main(String[] args) {
    CallHiddenImplBook kh = new CallHiddenImplBook();
    kh.doIt();
}
}

有许多不同的方法可以使用依赖注入。可以将其与 Singleton 等组合,但基本上只能通过在另一个对象中创建对象类型的属性来实现关联。 只有功能才有用,我们应该一遍又一遍地编写的代码总是为我们准备和完成。这就是为什么 DI 与控制反转 (IoC) 如此紧密地结合在一起的原因,这意味着我们的程序将控制权传递给另一个正在运行的模块,该模块将 bean 注入到我们的代码中。(每个可以注入的对象都可以被签名或视为一个 Bean。例如,在 Spring 中,它是通过创建和初始化 ApplicationContext 容器来完成的,它为我们完成了这项工作。我们只需在代码中创建 Context 并调用初始化 bean。在那一刻,注射已经自动完成。

17赞 Harleen 12/4/2015 #20

什么是依赖注入?

依赖注入(DI)是指将相互依赖的对象解耦。假设对象 A 依赖于对象 B,因此我们的想法是将这些对象彼此分离。我们不需要使用 new 关键字对对象进行硬编码,而是在运行时共享对对象的依赖关系,尽管有编译时间。 如果我们谈论

依赖注入在 Spring 中是如何工作的:

我们不需要使用 new 关键字对对象进行硬编码,而是在配置文件中定义 bean 依赖项。弹簧容器将负责连接所有容器。

控制反转 (IOC)

IOC 是一个通用概念,它可以用许多不同的方式表达,依赖注入是 IOC 的一个具体例子。

依赖注入的两种类型:

  1. 构造函数注入
  2. 孵化器注入

1. 基于构造函数的依赖注入:

当容器调用具有多个参数的类构造函数时,就会完成基于构造函数的 DI,每个参数表示对其他类的依赖关系。

public class Triangle {

private String type;

public String getType(){
    return type;
 }

public Triangle(String type){   //constructor injection
    this.type=type;
 }
}
<bean id=triangle" class ="com.test.dependencyInjection.Triangle">
        <constructor-arg value="20"/>
  </bean>

2. 基于 Setter 的依赖注入:

基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 Bean 后在 Bean 上调用 setter 方法来完成的。

public class Triangle{

 private String type;

 public String getType(){
    return type;
  }
 public void setType(String type){          //setter injection
    this.type = type;
  }
 }

<!-- setter injection -->
 <bean id="triangle" class="com.test.dependencyInjection.Triangle">
        <property name="type" value="equivialteral"/>

注意: 将构造函数参数用于必需依赖项,将 setter 用于可选依赖项是一个很好的经验法则。请注意,如果我们使用基于 setter 的注解,而不是 setter 上的@Required注解可用于将 setter 作为必需的依赖项。

9赞 Nikos M. 12/10/2015 #21

依赖注入是指一种方式(实际上是任何方式),让代码的一部分(例如一个类)以模块化的方式访问依赖项(代码的其他部分,例如它所依赖的其他类),而无需对它们进行硬编码(因此它们可以自由更改或覆盖,甚至可以根据需要在其他时间加载)

(和 PS ,是的,对于一个相当简单的概念来说,它已经成为一个过度炒作的 25 美元名称),我的美分.25

15赞 Anwar Husain 4/6/2016 #22

我能想到的最好的类比是手术室里的外科医生和他的助手,外科医生是主要人物,他的助手在他需要时提供各种手术组件,以便外科医生可以专注于他最擅长的一件事(手术)。没有助手,外科医生每次需要时都必须自己拿到组件。

简称 DI,是一种通过向组件提供依赖组件来消除组件获取依赖组件的常见额外责任(负担)的技术。

DI 使您更接近单一责任 (SR) 原则,例如 .surgeon who can concentrate on surgery

何时使用 DI :我建议在几乎所有生产项目(小型/大型)中使用 DI,尤其是在不断变化的商业环境中:)

为什么 :因为你希望你的代码易于测试、可模拟等,以便你可以快速测试你的更改并将其推向市场。此外,当你有很多很棒的免费工具/框架来支持你进入一个拥有更多控制权的代码库时,你为什么不呢?

评论

0赞 Anwar Husain 5/18/2016
@WindRider谢谢。我完全同意。人的生命和人体是卓越设计的典范。脊柱是 ESB :)的一个很好的例子......
120赞 Peyman Mohamadpour 5/5/2016 #23

这是我见过的关于依赖注入依赖注入容器的最简单的解释:

无依赖注入

  • 应用程序需要 Foo(例如控制器),因此:
  • 应用程序创建 Foo
  • 应用程序调用 Foo
    • Foo 需要 Bar(例如服务),因此:
    • Foo 创建酒吧
    • Foo calls 酒吧
      • Bar 需要 Bim(服务、存储库、 ...),所以:
      • Bar 创建 Bim
      • 酒吧做点什么

使用依赖注入

  • 应用程序需要 Foo,这需要 Bar,需要 Bim,所以:
  • 应用程序创建 BIM
  • 应用程序创建 Bar 并为其提供 Bim
  • 应用程序创建 Foo 并为其提供 Bar
  • 应用程序调用 Foo
    • Foo calls 酒吧
      • 酒吧做点什么

使用依赖注入容器

  • 应用程序需要 Foo so:
  • 应用程序从容器中获取 Foo,因此:
    • 容器创建 BIM
    • 容器创建 Bar 并为其提供 Bim
    • 容器创建 Foo 并为其提供 Bar
  • 应用程序调用 Foo
    • Foo calls 酒吧
      • 酒吧做点什么

依赖注入和依赖注入容器是不同的东西:

  • 依赖注入是一种编写更好代码的方法
  • DI 容器是一种帮助注入依赖关系的工具

不需要容器来执行依赖项注入。但是,容器可以为您提供帮助。

评论

0赞 David 7/5/2022
恕我直言,这里最好的答案,因为它没有将 ioc 或构造函数注入与 di 混合。
4赞 kusnaditjung tjung 6/4/2016 #24

依赖注入 (DI) 是依赖反转原则 (DIP) 实践的一部分,也称为控制反转 (IoC)。基本上,你需要做 DIP,因为你想让你的代码更加模块化和单元可测试,而不仅仅是一个单体系统。因此,您开始识别可以从类中分离出来并抽象掉的代码部分。现在,抽象的实现需要从类外部注入。通常,这可以通过构造函数来完成。因此,您创建了一个接受抽象作为参数的构造函数,这称为依赖注入(通过构造函数)。有关 DIP、DI 和 IoC 容器的更多说明,您可以阅读 此处

47赞 wakqasahmed 10/29/2016 #25

使依赖注入概念易于理解。让我们以切换(打开/关闭)灯泡的开关按钮为例。

无依赖注入

Switch 需要事先知道我连接到哪个灯泡(硬编码依赖项)。所以

Switch -> PermanentBulb //开关直接连接到永久灯泡,不容易测试

Switch(){
PermanentBulb = new Bulb();
PermanentBulb.Toggle();
}

使用依赖注入

Switch 只知道我需要打开/关闭传递给我的灯泡。所以

切换 -> Bulb1 或 Bulb2 或 NightBulb(注入依赖项)

Switch(AnyBulb){ //pass it whichever bulb you like
AnyBulb.Toggle();
}

修改开关和灯泡的 James 示例:

public class SwitchTest { 
  TestToggleBulb() { 
    MockBulb mockbulb = new MockBulb(); 

    // MockBulb is a subclass of Bulb, so we can 
    // "inject" it here: 
    Switch switch = new Switch(mockBulb); 

    switch.ToggleBulb(); 
    mockBulb.AssertToggleWasCalled(); 
  } 
}

public class Switch { 
  private Bulb myBulb; 

  public Switch() { 
    myBulb = new Bulb(); 
  } 

  public Switch(Bulb useThisBulbInstead) { 
    myBulb = useThisBulbInstead; 
  } 

  public void ToggleBulb() { 
    ... 
    myBulb.Toggle(); 
    ... 
  } 
}`
1赞 Ciro Corvino 11/18/2016 #26

依赖注入是“控制反转”原则的一种实现,基于框架构建。

GoF的“设计模式”中所述的框架是实现主控制流逻辑的类,使开发人员能够做到这一点,这样框架就实现了控制原理的反转。

一种实现为技术而不是类层次结构的方法,这个 IoC 原则它只是依赖注入。

DI 主要包括将类实例的映射和对该实例的类型引用委托给外部“实体”:对象、静态类、组件、框架等......

类实例是“依赖关系”,调用组件与类实例的外部绑定通过引用是“注入”。

显然,从 OOP 的角度来看,你可以以多种方式实现这种技术,例如构造函数注入、setter 注入、接口注入

委托第三方执行将 ref 与对象匹配的任务,当您想要将需要某些服务的组件与相同的服务实现完全分离时,这非常有用。

这样,在设计组件时,您可以只关注它们的架构和特定逻辑,信任用于与其他对象协作的接口,而不必担心所使用的对象/服务的任何类型的实现更改,即使您使用的同一对象将被完全替换(显然尊重接口)。

0赞 Hisham Javed 3/27/2017 #27

任何重要的应用程序都由两个或多个类组成,这些类相互协作以执行某些业务逻辑。传统上,每个对象负责获取自己对与之协作的对象(其依赖项)的引用。应用 DI 时,对象在创建时由协调系统中每个对象的某个外部实体赋予其依赖关系。换句话说,依赖项被注入到对象中。

有关更多详细信息,请参阅在此处输入链接说明

199赞 user2771704 7/6/2017 #28

让我们尝试一下 CarEngine 类的简单示例,任何汽车都需要引擎才能去任何地方,至少现在是这样。因此,下面代码在没有依赖注入的情况下会是什么样子。

public class Car
{
    public Car()
    {
        GasEngine engine = new GasEngine();
        engine.Start();
    }
}

public class GasEngine
{
    public void Start()
    {
        Console.WriteLine("I use gas as my fuel!");
    }
}

为了实例化 Car 类,我们将使用以下代码:

Car car = new Car();

我们与 GasEngine 紧密耦合的这段代码的问题,如果我们决定将其更改为 ElectricityEngine,那么我们将需要重写 Car 类。应用程序越大,我们将不得不添加和使用新型引擎的问题和头痛就越多。

换句话说,使用这种方法时,我们的高级 Car 类依赖于较低级别的 GasEngine 类,这违反了 SOLID 的依赖反转原则 (DIP)。DIP 建议我们应该依赖抽象,而不是具体的类。因此,为了满足这一点,我们引入了 IEngine 接口并重写了如下代码:

    public interface IEngine
    {
        void Start();
    }

    public class GasEngine : IEngine
    {
        public void Start()
        {
            Console.WriteLine("I use gas as my fuel!");
        }
    }

    public class ElectricityEngine : IEngine
    {
        public void Start()
        {
            Console.WriteLine("I am electrocar");
        }
    }

    public class Car
    {
        private readonly IEngine _engine;
        public Car(IEngine engine)
        {
            _engine = engine;
        }

        public void Run()
        {
            _engine.Start();
        }
    }

现在我们的 Car 类只依赖于 IEngine 接口,而不是引擎的特定实现。 现在,唯一的诀窍是如何创建一个 Car 的实例,并给它一个实际的具体 Engine 类,如 GasEngine 或 ElectricityEngine。这就是依赖注入的用武之地。

   Car gasCar = new Car(new GasEngine());
   gasCar.Run();
   Car electroCar = new Car(new ElectricityEngine());
   electroCar.Run();

在这里,我们基本上将我们的依赖项(引擎实例)注入(传递)到 Car 构造函数。因此,现在我们的类在对象及其依赖项之间具有松散耦合,并且我们可以轻松地添加新类型的引擎,而无需更改 Car 类。

依赖注入的主要好处是类的耦合更加松散,因为它们没有硬编码的依赖关系。这遵循了上面提到的依赖反转原则。类不是引用特定的实现,而是请求抽象(通常是接口),这些抽象在构造类时提供给它们。

所以归根结底,依赖注入只是一种技术 实现对象及其依赖项之间的松散耦合。 而不是直接实例化类需要的依赖项 为了执行其操作,向类提供了依赖项 (最常见)通过构造函数注入。

此外,当我们有许多依赖项时,使用控制反转 (IoC) 容器是非常好的做法,我们可以告诉哪些接口应该映射到我们所有依赖项的哪些具体实现,我们可以让它在构造我们的对象时为我们解决这些依赖项。例如,我们可以在 IoC 容器的映射中指定应将 IEngine 依赖项映射到 GasEngine 类,当我们向 IoC 容器请求 Car 类的实例时,它将自动构造带有 GasEngine 依赖项的 Car 类。

更新:最近观看了 Julie Lerman 关于 EF Core 的课程,也喜欢她对 DI 的简短定义。

依赖项注入是一种允许应用程序注入的模式 对象动态地发送到需要它们的类,而不强制这些对象 负责这些对象的类。它允许你的代码 更松散的耦合,并且 Entity Framework Core 插入到相同的 服务系统。

评论

4赞 elixir 1/25/2018
只是出于好奇,这与策略模式有何不同?此模式封装了算法并使其可互换。感觉依赖注入和策略模式非常相似。
28赞 SAMUEL 11/3/2017 #29

以上答案都很好,我的目的是用简单的方式解释这个概念,让任何没有编程知识的人也能理解这个概念

依赖注入是帮助我们以更简单的方式创建复杂系统的设计模式之一。

我们可以看到这种模式在我们的日常生活中的各种应用。 一些例子是录音机、VCD、CD 驱动器等。

Reel-to-reel portable tape recorder, mid-20th century.

上图是 20 世纪中叶卷对卷便携式录音机的图像。来源

录音机的主要目的是录制或播放声音。

在设计系统时,它需要一个卷轴来录制或播放声音或音乐。设计此系统有两种可能性

  1. 我们可以将卷轴放入机器内
  2. 我们可以为卷轴提供一个钩子,可以放置它。

如果我们使用第一个,我们需要打开机器来更换卷轴。 如果我们选择第二种,即为卷轴放置一个钩子,我们将通过更换卷轴获得播放任何音乐的额外好处。并且还将功能简化为仅播放卷轴中的任何内容。

与明智的依赖注入一样,依赖注入是将依赖关系外部化以仅关注组件的特定功能的过程,以便可以将独立组件耦合在一起以形成一个复杂的系统。

我们通过使用依赖注入获得的主要好处。

  • 高内聚力和松耦合。
  • 将依赖性外化,只看责任。
  • 将事物作为组件并组合起来形成具有高性能的大型系统。
  • 它有助于开发高质量的组件,因为它们是独立开发的,并且经过适当的测试。
  • 如果一个组件出现故障,它有助于用另一个组件替换该组件。

现在,这些概念构成了编程世界中众所周知的框架的基础。 Spring Angular 等是建立在这个概念之上的著名软件框架

依赖关系注入是一种模式,用于创建其他对象所依赖的对象实例,而不知道在编译时将使用哪个类来提供该功能,或者简单地将属性注入到对象的方式称为依赖关系注入。

依赖注入示例

以前我们编写的代码是这样的

Public MyClass{
 DependentClass dependentObject
 /*
  At somewhere in our code we need to instantiate 
  the object with new operator  inorder to use it or perform some method.
  */ 
  dependentObject= new DependentClass();
  dependentObject.someMethod();
}

使用 Dependency 注入,依赖注入器将为我们取消实例化

Public MyClass{
 /* Dependency injector will instantiate object*/
 DependentClass dependentObject

 /*
  At somewhere in our code we perform some method. 
  The process of  instantiation will be handled by the dependency injector
 */ 
   
  dependentObject.someMethod();
}

您还可以阅读

控制反转与依赖注入之间的区别

20赞 Linh 3/14/2018 #30

例如,我们有 2 个类和 . 将使用ClientServiceClientService

public class Service {
    public void doSomeThingInService() {
        // ...
    }
}

无依赖注入

方式 1)

public class Client {
    public void doSomeThingInClient() {
        Service service = new Service();
        service.doSomeThingInService();
    }
}

方式 2)

public class Client {
    Service service = new Service();
    public void doSomeThingInClient() {
        service.doSomeThingInService();
    }
}

方式 3)

public class Client {
    Service service;
    public Client() {
        service = new Service();
    }
    public void doSomeThingInClient() {
        service.doSomeThingInService();
    }
}

1) 2) 3) 使用

Client client = new Client();
client.doSomeThingInService();

优势

  • 简单

  • 考试类很难Client
  • 当我们更改构造函数时,我们需要更改所有创建对象的代码ServiceService

使用依赖注入

方式 1)构造函数注入

public class Client {
    Service service;

    Client(Service service) {
        this.service = service;
    }

    // Example Client has 2 dependency 
    // Client(Service service, IDatabas database) {
    //    this.service = service;
    //    this.database = database;
    // }

    public void doSomeThingInClient() {
        service.doSomeThingInService();
    }
}

Client client = new Client(new Service());
// Client client = new Client(new Service(), new SqliteDatabase());
client.doSomeThingInClient();

方式 2)孵化器注入

public class Client {
    Service service;

    public void setService(Service service) {
        this.service = service;
    }

    public void doSomeThingInClient() {
        service.doSomeThingInService();
    }
}

Client client = new Client();
client.setService(new Service());
client.doSomeThingInClient();

方式 3)接口注入

检查 https://en.wikipedia.org/wiki/Dependency_injection

===

现在,这段代码已经被遵循了,测试类更容易。
但是,我们仍然使用了很多时间,并且在更改构造函数时并不好。为了防止它,我们可以像 1) 简单的
手动一样使用 DI 喷油器
Dependency InjectionClientnew Service()ServiceInjector

public class Injector {
    public static Service provideService(){
        return new Service();
    }

    public static IDatabase provideDatatBase(){
        return new SqliteDatabase();
    }
    public static ObjectA provideObjectA(){
        return new ObjectA(provideService(...));
    }
}

Service service = Injector.provideService();

2)使用库:适用于Android dagger2

优势

  • 让测试更简单
  • 当您更改 时,您只需要在 Injector 类中更改它Service
  • 如果你使用 use ,当你查看 的构造函数时,你会看到类有多少依赖Constructor InjectionClientClient

  • 如果你使用 use ,对象是在创建时创建的,有时我们在类中使用函数而不使用,所以创建是浪费的Constructor InjectionServiceClientClientServiceService

依赖注入定义

https://en.wikipedia.org/wiki/Dependency_injection

依赖项是可以使用的对象 () 注入是将依赖项 () 传递给使用它的依赖对象 ()
ServiceServiceClient

4赞 H S W 7/2/2018 #31

摘自 Christoffer Noring,Pablo Deeleman 的著作“Learning Angular - Second Edition”:

“随着我们的应用程序的增长和发展,我们的每个代码实体在内部都需要其他对象的实例,这些对象在软件工程领域更广为人知的是依赖关系。将此类依赖项传递给依赖客户端的操作称为注入,它还需要另一个代码实体(称为注入器)的参与。注入器将负责实例化引导所需的依赖项,以便它们从成功注入客户端的那一刻起就可以使用。这非常重要,因为客户端对如何实例化自己的依赖项一无所知,并且只知道它们实现的接口才能使用它们。

来自:安东·莫伊谢耶夫。《Angular Development with Typescript, Second Edition》一书:

“简而言之,DI 可以帮助您以松散耦合的方式编写代码,并使您的代码更具可测试性和可重用性。”

4赞 adamw 9/21/2018 #32

我会对依赖注入提出一个稍微不同、简短而精确的定义,重点是主要目标,而不是技术手段(从这里开始):

依赖注入是创建静态、无状态的过程 服务对象的图形,其中每个服务都由其 依赖。

我们在应用程序中创建的对象(无论我们使用的是 Java、C# 还是其他面向对象的语言)通常属于以下两类之一:无状态、静态和全局“服务对象”(模块),以及有状态、动态和本地“数据对象”。

模块图(服务对象图)通常在应用程序启动时创建。这可以使用容器(如 Spring)来完成,但也可以通过将参数传递给对象构造函数来手动完成。这两种方法都有其优点和缺点,但在应用程序中使用 DI 绝对不需要框架。

一个要求是服务必须由其依赖关系进行参数化。这究竟意味着什么取决于给定系统中采用的语言和方法。通常,这采用构造函数参数的形式,但使用 setter 也是一种选择。这也意味着服务的依赖关系对服务的用户是隐藏的(在调用服务方法时)。

何时使用?我想说的是,每当应用程序足够大,将逻辑封装到单独的模块中时,模块之间的依赖关系图可以提高代码的可读性和可探索性。

1赞 DevTheJo 9/27/2018 #33

依赖注入是使解耦组件与其某些依赖无关的做法,这遵循 SOLID 指南,即

依赖反转原则:人们应该“依赖于抽象, 不是凝结物。

依赖关系注入的更好实现是组合根设计模式,因为它允许组件与依赖关系注入容器分离。

我推荐这篇由 Mark Seemann 撰写的关于 Composition Root http://blog.ploeh.dk/2011/07/28/CompositionRoot/ 的精彩文章

以下是本文的要点:

组合根是应用程序中(最好)唯一的位置 其中模块组合在一起。

...

只有应用程序才应具有组合根。库和 框架不应该。

...

DI 容器只能从组合根引用。 所有其他模块都不应引用容器。

Di-Ninja(一个依赖注入框架)的文档是一个很好的例子,它演示了组合根和依赖注入的原则是如何工作的。https://github.com/di-ninja/di-ninja据我所知,是 javascript 中唯一一个实现 Composition-Root 设计模式的 DiC。

0赞 Shadi Alnamrouti 1/5/2019 #34

DI 是真实对象如何实际交互,而一个对象不对另一个对象的存在负责。对象应平等对待。它们都是对象。任何人都不应该表现得像一个创造者。这就是你如何公正地对待你的对象。

简单示例

如果你需要一个医生,你只需去找一个(现有的)医生。您不会考虑从头开始创建医生来帮助您。他已经存在了,他可以为你或其他对象服务。无论你(单个对象)是否需要他,他都有权利存在,因为他的目的是为一个或多个对象服务。决定他存在的人是全能的上帝,而不是自然选择。因此,DI的一个优点是避免在宇宙的生命周期(即应用)中创造无用的冗余物体,这些物体没有目的。

6赞 Nithin Prasad 9/7/2019 #35

5岁儿童的依赖注射。

当你自己去把东西从冰箱里拿出来时,你可能会引起问题。你可能会把门敞开着,你可能会得到妈妈或爸爸不想让你拥有的东西。您甚至可能在寻找我们甚至没有或已经过期的东西。

你应该做的是说明一个需求,“我需要在午餐时喝点东西”,然后我们会确保你在坐下来吃饭时有东西。

评论

0赞 Codor 9/10/2020
我不确定这种解释是否具有误导性,因为不利的服务定位器反模式似乎是所描述问题的有效解决方案。
1赞 王玉略 1/21/2020 #36

我们可以实现依赖注入来了解它:

class Injector {
  constructor() {
    this.dependencies = {};
    this.register = (key, value) => {
      this.dependencies[key] = value;
    };
  }
  resolve(...args) {
    let func = null;
    let deps = null;
    let scope = null;
    const self = this;
    if (typeof args[0] === 'string') {
      func = args[1];
      deps = args[0].replace(/ /g, '').split(',');
      scope = args[2] || {};
    } else {
      func = args[0];
      deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
      scope = args[1] || {};
    }
    return (...args) => {
      func.apply(scope || {}, deps.map(dep => self.dependencies[dep] && dep != '' ? self.dependencies[dep] : args.shift()));
    }
  }
}

injector = new Injector();

injector.register('module1', () => { console.log('hello') });
injector.register('module2', () => { console.log('world') });

var doSomething1 = injector.resolve(function (module1, module2, other) {
  module1();
  module2();
  console.log(other);
});
doSomething1("Other");

console.log('--------')

var doSomething2 = injector.resolve('module1,module2,', function (a, b, c) {
  a();
  b();
  console.log(c);
});
doSomething2("Other");

以上是 JavaScript 的实现

104赞 Gk Mohammad Emon 1/27/2020 #37

在进行技术描述之前,首先用一个现实生活中的例子来可视化它,因为你会发现很多技术的东西来学习依赖注入,但大多数人无法获得它的核心概念。

在第一张图片中,假设您有一家拥有大量设备的汽车工厂。汽车实际上是在装配单元中制造的,但它需要发动机座椅车轮。因此,装配单元依赖于这些所有单元,它们是工厂的依赖项

您可能会感觉到,现在维护该工厂的所有任务太复杂了,因为除了主要任务(在装配单元中组装汽车)外,您还必须关注其他单元。现在的维护成本非常高,而且厂房很大,所以租金需要额外的钱。

现在,请看第二张图片。如果您找到一些供应商公司以比您自产成本更低的价格为您提供车轮座椅发动机,那么现在您不需要在工厂制造它们。您现在可以为您的装配单元租用较小的建筑物,这将减少您的维护任务并降低您的额外租赁成本。现在,您也可以只专注于您的主要任务(汽车组装)。

现在我们可以说,组装汽车的所有依赖项都是从供应商那里注入到工厂的。这是现实生活中依赖注入 (DI) 的一个例子。

现在,在技术术语中,依赖注入是一种技术,其中一个对象(或静态方法)提供另一个对象的依赖关系。因此,将创建对象的任务转移给其他人并直接使用依赖关系称为依赖关系注入。

将帮助您现在通过技术解释来学习 DI。这将显示何时使用 DI 以及何时不应使用 DI。

All in one car factory.

Simple car factory

评论

6赞 Bryan Green 3/11/2022
为这个概念的视觉表现点赞!
-3赞 William Deans 3/15/2023 #38

除非你真的需要它,否则依赖注入实际上是可怕的,因为它将 HelloWorld() 变成 HelloWorld( CharacterEncoding, Language, Logger 等) 抛出 InvalidEncodingException、UnknownLanguageException、DiskFullException 等。现在,您必须创建所有这些东西并处理记录器填满磁盘的错误,即使没有实际使用记录器的实现。随着代码的增长,您最终可能会得到 1000 个或更多参数......真的没有限制。然后,您必须为使用容器创建的新问题实现解决方案。人们最终会为界面中未使用的方面编写测试代码。总工作量很容易增加。

这是一种非常好的方法,可以在需要时使用,但是当您实际上不需要交换依赖项时它的生命就太短了。因此,只有在有充分理由的情况下,才要小心使用;不仅仅是因为你听说这是“好习惯”。

评论

0赞 E_net4 3/19/2023
这个答案似乎是由 DI 执行不佳支持,而不是模式本身的问题。“HelloWorld( CharacterEncoding, Language, Logger, etc ) throws InvalidEncodingException, UnknownLanguageException” 这些不应该发生,除非您在构造函数中创建此类依赖项,这不是 DI。“即使没有实际使用记录器实现,也要处理记录器填满磁盘的错误” 否,不依赖于记录器的组件不会处理记录器错误,也不会成为记录器被实例化的原因。throws
0赞 Burak 4/9/2023 #39

什么是“依赖”?

当您在类中使用对象时,该类将依赖于该对象。这样,该对象将成为类的依赖项。

class UserService {

    private var userRepository: UserRepository()

    fun getUser(id: Int): User {
        return userRepository.getUser(id)
    }

}

在此示例中,object 是类的依赖项。userRepositoryUserService

什么是“依赖注入”?

类需要到达某些对象。您可以在类中创建对象,也可以通过构造函数、方法等传递(注入)类所需的对象(依赖项)。当您以某种方式传递对象时,它称为依赖注入。就这么简单。

最有可能的是,你已经在不知情的情况下进行了依赖注入。

依赖注入有三种类型:构造函数注入、setter 注入和接口注入。

你可以从我的中篇文章中阅读更多内容。

你也可以看到我的依赖注入框架