什么是依赖注入?

3608

已经有一些关于依赖注入的问题被提出,例如何时使用它以及有哪些框架可用。然而,

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


请看我对依赖注入的讨论这里 - Kevin S.
44
我同意关于链接的评论。我可以理解你可能想引用别人的内容,但请至少说明为什么要链接他们以及这个链接相比使用谷歌搜索得到的其他链接有何优势。 - Christian Payne
技术上讲,依赖注入并不是IoC的一种特殊形式。相反,IoC是提供依赖注入的一种技术。其他技术也可以用来提供依赖注入(尽管IoC是常见的唯一一种),而且IoC也被用于解决许多其他问题。 - Sean Reilly
157
关于链接,请记住它们经常会以某种方式消失。 Stack Overflow 的答案中有越来越多的死链接。因此,即使链接的文章再好,如果你找不到它,那么它就毫无用处。 - DOK
Vojta Jina关于依赖注入的视频http://youtu.be/_OGGsf1ZXMs。第一部分。 - Jichao
依赖注入的概述及其与其他面向对象编程原则的关系:http://deviq.com/dependency-injection/ - ssmith
39个回答

2787

到目前为止我找到的最好的定义是James Shore提出的

"依赖注入"是一个价值25美元的术语,用来描述一个价值5美分的概念。[...] 依赖注入意味着给对象其实例变量。[...]

Martin Fowler撰写的一篇文章也可能会有所帮助。

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

依赖项可以通过多种方式(如构造函数注入或设置器注入)注入到对象中。甚至可以使用专门的依赖注入框架(例如Spring)来完成这个过程,但它们并不是必需的。你不需要这些框架来进行依赖注入。显式地实例化和传递对象(依赖项)同样可以完成注入的过程。


60
我喜欢James文章中的解释,特别是结尾部分:“尽管如此,你不得不惊叹于采用了三个概念(“TripPlanner”,“CabAgency”和“AirlineAgency”),转化为九个以上的类,并在编写单个应用程序逻辑之前添加了数十行粘合代码和配置XML的任何方法。”这是我经常看到的情况(可悲的是)- 依赖注入(本身很好,正如他所解释的那样)被误用来使本该更简单完成的事情过度复杂化,最终只能写出“支持”代码... - Matt
6
“显式实例化和传递对象(依赖项)与框架注入一样好。”那么,为什么人们要使用框架进行注入呢? - dzieciou
27
每个框架被编写的原因都是相同的(或者至少应该是相同的):因为一旦你达到了某个复杂程度,就需要编写大量重复/样板代码。问题在于,很多时候即使没有必要,人们也会使用框架。 - Thiago Arrais
28
“$25 term for a 5-cent concept is dead on.” 的翻译是:“一个五分概念的25美元术语确实准确。” 这里有一篇很好的文章可以帮助我理解:http://www.codeproject.com/Articles/615139/An-Absolute-Beginners-Tutorial-on-Dependency-Inver。 - Christine
2
@dzieciou,当你使用 DI 容器时,构建对象图形并能够在一个地方轻松替换一种实现方式也是非常好的。通常对于愚蠢、简单的东西,我可能会传递依赖项,但在大多数框架中使用 DI 容器也同样容易。 - M H

2159

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

依赖注入使得测试变得更加容易。可以通过构造函数进行注入。

SomeClass()的构造函数如下:

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

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

替代方案

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

myObject可以直接传递,这使得测试更加容易。

  • 一个常见的替代方案是定义一个空构造函数。依赖注入可以通过setter方法来完成。(感谢@MikeVella)
  • Martin Fowler记录了第三种替代方案(感谢@MarcDix),其中类显式实现一个接口,以便注入程序员希望注入的依赖项。

如果没有依赖注入,单元测试中隔离组件就会更加困难。

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


30
承认引用Ben Hoffstein对Martin Fowler文章的提及是必要的,因为它指出这个主题的“必读”文章。我接受wds的答案,因为它实际上回答了这里在SO上的问题。 - AR.
138
+1 解释和动机:将类所依赖的对象创建交给其他人处理。另一种说法是,DI 使类更具凝聚力(它们有更少的责任)。 - Fuhrmanator
15
您说依赖项是通过“构造函数”传递的,但据我所知,这并不完全正确。如果依赖关系在对象实例化后设置为属性,则仍然属于依赖注入,对吗? - Mike Vella
2
@MikeVella 是的,那是正确的。在大多数情况下,这并没有真正的区别,尽管属性通常更加灵活。我会稍微编辑一下文本来指出这一点。 - wds
3
迄今为止我发现的最好的答案之一,因此我非常有兴趣改进它。它缺少对第三种依赖注入形式的描述:接口注入 - Marc Dix
显示剩余6条评论

830

我在关于松耦合的维基百科上发现了这个有趣的例子:

来源:理解依赖注入

任何应用都由许多对象协作完成一些有用的事情。传统上,每个对象负责获取与之协作的依赖对象(依赖项)的引用。这导致类之间高度耦合和难以测试的代码。

例如,考虑一个Car对象。

一辆汽车需要轮子、发动机、燃料、电池等等才能运行。传统上,我们在Car对象的定义中同时定义这些依赖对象的品牌。

没有依赖注入(DI):

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

  //The rest
}

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

如果我们想在最初的NepaliRubberWheel()失效后更改其依赖对象类型 - 比如Wheel - 怎么办呢? 我们需要用新的依赖ChineseRubberWheel()重新创建Car对象,但只有Car制造商才能这样做。

那么Dependency Injection为我们做了什么呢...?

使用依赖注入时,对象是在运行时而不是编译时(汽车制造时)获得其依赖关系。 因此,我们现在可以随时更改Wheel。在这里,dependencywheel)可以在运行时注入到Car中。

使用依赖注入后:

我们在运行时注入依赖项(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;
  }
}

优点:

  • 解耦对象的创建(换句话说,将使用与对象创建分开)
  • 能够替换依赖关系(例如:轮子、电池),而不必更改使用它的类(汽车)
  • 促进“按接口编程而非按实现编程”的原则
  • 在测试期间能够创建和使用模拟依赖项(如果我们想在测试期间使用 Wheel 的模拟对象而不是真实实例,则可以创建模拟 Wheel 对象并让 DI 框架注入到 Car 中)

29
我的理解是,与其将一个新对象实例化为另一个对象的一部分,我们可以在需要时注入这个对象,从而消除第一个对象对它的依赖。这样说对吗? - JeliBeanMachine
14
个人很喜欢这个比喻,因为它用简单易懂的说法来解释。假设我是丰田汽车,已经在设计到生产线制造这辆车上花费了大量资金和人力成本。如果已经存在信誉良好的轮胎厂商,那我为什么要从头开始建立一个轮胎制造部门呢?也就是说,“新建”一个轮胎制造部门。我不会这样做。我所需要做的就是从他们那里购买(通过参数注入)轮胎,安装好,完美!回到编程方面,假设一项C#项目需要使用现有的库/类,有两种运行/调试方法:1-将整个库添加到该项目的引用中; - Jeb50
(续)...外部库/类,或者2-从DLL中添加它。除非我们需要查看这个外部类的内部内容,将其作为DLL添加是更简单的方法。因此,选项1是使用“new”进行实例化,选项2是将其作为参数传递。可能不太准确,但很简单易懂。 - Jeb50
3
@JeliBeanMachine (很抱歉回复评论晚了...)我们并不是移除第一个对象对轮子对象或电池对象的依赖关系,而是传递给它这些依赖关系,以便我们可以更改依赖关系的实例或实现。之前:汽车对尼泊尔橡胶轮子有硬编码的依赖关系。之后:汽车对一个轮子实例有注入式的依赖关系。 - Mikael Ohlson
很棒的解释。这看起来像是将组合更改为聚合,因此汽车不拥有轮胎,只是使用它们。 - Raymond Chen
显示剩余2条评论

295

依赖注入(Dependency Injection)是一种实践,它使对象以一种方式设计,从其他代码片段中接收对象的实例而不是在内部构造它们。这意味着任何实现所需接口的对象都可以替换进来,而无需更改代码,这简化了测试并提高了解耦性。

例如,请考虑以下类:

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 ) { ... }
} 
在这个示例中,PersonService::addManagerPersonService::removeManager的实现需要一个GroupMembershipService的实例才能正常工作。如果没有使用依赖注入,传统的做法是在PersonService的构造函数中实例化一个新的GroupMembershipService并在两个函数中使用该实例属性。然而,如果GroupMembershipService的构造函数有多个要求的参数,或者更糟糕的是,有一些需要调用GroupMembershipService的初始化“setter”的情况,代码会很快变得臃肿,并且PersonService不仅依赖于GroupMembershipService,还依赖于GroupMembershipService所依赖的一切。此外,与GroupMembershipService的链接被硬编码到PersonService中,这意味着您无法为测试目的“模拟”GroupMembershipService,或在应用程序的不同部分中使用策略模式。
使用依赖注入,而不是在PersonService内部实例化GroupMembershipService,您可以将其传递给PersonService构造函数,或者添加一个属性(getter和setter)来设置其本地实例。这意味着您的PersonService不再需要担心如何创建GroupMembershipService,它只接受给定的对象,并与它们一起工作。这也意味着任何是GroupMembershipService子类或实现了GroupMembershipService接口的东西都可以被“注入”到PersonService中,而PersonService并不需要知道这种变化。

37
如果您在使用 DI 后能提供相同的代码示例,那就太好了。 - CodyBugstein
2
这也意味着任何继承自GroupMembershipService类或实现GroupMembershipService接口的内容都可以被“注入”到PersonService中,而PersonService不需要知道这些变化。这对我来说是一个非常有用的收获 - 谢谢! - DefinedRisk

200

这个被接受的答案是不错的,但我想要补充的是,依赖注入非常类似于经典的避免在代码中硬编码常量。

当您使用某些常量比如数据库名称时,您很快会将其从代码内部移动到一些配置文件中,并传递一个包含该值的变量到需要它的地方。做这件事的原因是,这些常量通常比代码的其他部分更频繁地发生更改。例如,如果您想在测试数据库中测试代码。

在面向对象编程的世界中,DI类似于此。那里的值不是常量文字,而是整个对象。但是将创建这些对象的代码从类代码中移出的原因相似——对象的更改频率比使用它们的代码更高。其中一个重要的情况是测试。


22
“对象的更改频率比使用它们的代码更高。”为了概括,可以在流动点添加间接性。根据流动点的不同,这些间接性具有不同的名称!! - Chethan

199

让我们尝试一个简单的例子,涉及到 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的实例并给它一个真正的具体引擎类,比如GasEngine或ElectricityEngine。这就是依赖注入的作用所在。
   Car gasCar = new Car(new GasEngine());
   gasCar.Run();
   Car electroCar = new Car(new ElectricityEngine());
   electroCar.Run();

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

依赖注入的主要好处是,类之间的耦合性更低,因为它们没有硬编码的依赖项。这遵循了上面提到的依赖倒置原则。类请求抽象(通常是接口),而不是引用特定的实现,在类构造时提供给它们。

因此,依赖注入最终只是实现对象与其依赖项之间松耦合的一种技术手段。相比于直接实例化类需要的依赖项以执行其操作,依赖项(通常)通过构造函数注入提供给类。

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

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

依赖注入是一种模式,允许您的应用程序在需要它们的类上动态注入对象,而不会强制那些类负责这些对象。它使代码的耦合度更低,Entity Framework Core也插入到这个系统服务中。


4
仅出于好奇,这与策略模式有何不同?此模式封装了算法并使它们可互换。感觉依赖注入和策略模式非常相似。 - elixir
这是一个很棒的答案。 - Bryan Green

136

假设你想去钓鱼:

  • 没有依赖注入,你需要自己处理一切。你需要找船、买渔竿、找饵料等等。当然这是可能的,但这会让你承担很多责任。在软件术语中,这意味着你需要执行查找所有这些事情。

  • 有了依赖注入,其他人将负责所有的准备工作,并为你提供所需的设备。你将被注入("be injected")船只、渔竿和饵料-全部准备就绪。


65
想象一下,如果你雇了一位水管工来重新装修你的浴室,结果他却说:“太好了,这是我需要你为我准备的工具和材料清单”。这难道不应该是水管工的工作吗? - jscs
因此,有人需要照顾一些与其无关的人,但仍然决定收集船、棍子和鱼饵清单 - 尽管已经准备好使用。 - user2537701
29
不,那是管道工雇主的工作。作为客户,您需要做水暖工作。为此,您需要一名水暖工人。水暖工需要使用工具。为了获得这些工具,它会从水暖公司获得配备。作为客户,您不想知道水暖工具体需要做什么或需要什么。作为水暖工人,您知道您需要什么,但只是想要完成工作而不必操心所有细节。作为水暖工人的雇主,在将他们派往客户家之前,您有责任为他们装备所需的工具和设备。 - sara
1
@KingOfAllTrades:当然,在某个时候,你必须有人雇用和装备管道工,否则你就没有管道工。但你不让客户去做这件事。客户只是要求一个管道工,并得到一个已经配备好所需工具的管道工。使用 DI,你仍然最终需要一些代码来满足依赖关系。但你将其与真正工作的代码分离开来。如果你将其发挥到极致,你的对象只需知道它们的依赖关系,而对象图构建发生在外部,通常在初始化代码中进行。 - cHao
1
@ThomasRones:差不多就是这样。实际上,有不同的方法将依赖项传递给对象。一种方法是在构造函数中将它们作为参数传递。另一种方法是使用setter方法(或某种注释)。有些人强烈推荐基于构造函数的DI(请参见http://odrotbohm.de/2013/11/why-field-injection-is-evil/)。 - Olivier Liechti
显示剩余2条评论

120

这里是我见过的关于依赖注入(Dependency Injection)依赖注入容器(Dependency Injection Container)最简单的解释:

没有使用依赖注入

  • 应用程序需要Foo(例如一个控制器),所以:
  • 应用程序创建Foo
  • 应用程序调用Foo
    • Foo需要Bar(例如一个服务),所以:
    • Foo创建Bar
    • Foo调用Bar
      • Bar需要Bim(一个服务,一个仓库等等),所以:
      • Bar创建Bim
      • Bar执行某些操作

使用依赖注入

  • 应用程序需要Foo,Foo需要Bar,Bar需要Bim,所以:
  • 应用程序创建Bim
  • 应用程序创建Bar并给它Bim
  • 应用程序创建Foo并给它Bar
  • 应用程序调用Foo
    • Foo调用Bar
      • Bar执行某些操作

使用依赖注入容器

  • 应用程序需要Foo,所以:
  • 应用程序从容器中获取Foo,所以:
    • 容器创建Bim
    • 容器创建Bar并给它Bim
    • 容器创建Foo并给它Bar
  • 应用程序调用Foo
    • Foo调用Bar
      • Bar执行某些操作

依赖注入(Dependency Injection)依赖注入容器(Dependency Injection Container)是不同的东西:

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

您不需要容器来进行依赖注入。但是容器可以帮助您。


在我看来,这是最好的答案,因为它没有将IOC或构造函数注入与DI混淆。 - David

104
在进行技术描述之前,请先通过一个现实生活的例子来形象化它,因为你会发现很多技术内容都与学习依赖注入有关,但大多数人无法理解其核心概念。
首先,假设你有一个拥有许多单元的汽车工厂。汽车实际上是在装配单元中建造的,但它需要发动机、座位以及轮子。因此,装配单元依赖于所有这些单元,它们是该工厂的依赖项。
你可以感觉到现在维护这个工厂的所有任务变得太复杂了,因为除了主要任务(在装配单元中组装汽车)外,你还必须关注其他单元。现在维护成本非常高,工厂建筑也很大,所以租金需要额外的开销。
现在看看第二张图片。如果你找到一些供应商公司,他们提供的轮子、座位和发动机比你自己生产的成本更低,那么现在你就不需要在自己的工厂里制造它们了。你现在可以租用一个较小的建筑物,只用于你的装配单元,这将减少你的维护任务并降低额外的租金成本。现在你也可以只关注于你的主要任务(汽车组装)。
现在我们可以说,所有组装汽车的依赖项都是从供应商那里“注入”到工厂中的。这是一个现实生活中的依赖注入(DI)例子。

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

将帮助您了解依赖注入的技术解释。将展示何时应该使用DI以及何时不应该使用。

All in one car factory.

Simple car factory


6
感谢你为这个概念提供了视觉呈现! - Bryan Green

69

“依赖注入”不就是使用带参数的构造函数和公共设置器吗?

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

Constructor without dependency injection:

public class Example { 
  private DatabaseThingie myDatabase; 

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

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

Constructor with dependency injection:

public class Example { 
  private DatabaseThingie myDatabase; 

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

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

在 DI 版本中,您肯定不想在无参数构造函数中初始化 myDatabase 对象吧?这似乎没有意义,并且如果您尝试调用 DoStuff 而没有调用重载的构造函数,它会抛出异常。 - Matt Wilko
只有当new DatabaseThingie()不能生成有效的myDatabase实例时。 - JaneGoodall

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