ChatGPT解决这个技术问题 Extra ChatGPT

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

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

请参阅我关于依赖注入 Here 的讨论。
我同意有关链接的评论。我可以理解您可能想参考其他人。但至少添加您链接它们的原因以及使该链接比我可以通过使用谷歌获得的其他链接更好的原因
@AR:从技术上讲,依赖注入不是 IoC 的一种特殊形式。相反,IoC 是一种用于提供依赖注入的技术。其他技术可用于提供依赖注入(尽管 IoC 是唯一常用的技术),并且 IoC 也用于许多其他问题。
关于链接,请记住它们经常以一种或另一种方式消失。 SO答案中有越来越多的死链接。所以,链接的文章再好,找不到也没有用。
Vojta Jina 关于依赖注入 youtu.be/_OGGsf1ZXMs。第一部分。

a
ardila

到目前为止,我发现的最佳定义是 one by James Shore

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

an article by Martin Fowler 也可能被证明是有用的。

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

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


我喜欢 James 文章的解释,尤其是结尾:“不过,你必须惊叹于任何采用三个概念('TripPlanner'、'CabAgency'和'AirlineAgency')的方法,将它们变成九个以上的类,然后在编写一行应用程序逻辑之前添加数十行粘合代码和配置 XML。”这是我经常看到的(可悲)——依赖注入(正如他所解释的那样本身很好)被滥用来使本来可以更容易完成的事情变得过于复杂——最终编写“支持”代码......
回复:“显式实例化和传递对象(依赖项)与框架注入一样好。”。那么为什么人们制作框架这样做呢?
出于同样的原因,每个框架都被编写(或至少应该被编写):因为一旦达到一定的复杂性,就需要编写大量重复的/样板代码。问题是很多时候人们会寻求一个框架,即使它不是严格需要的。
5 美分概念的 25 美元期限已过时。这是一篇对我有帮助的好文章:codeproject.com/Articles/615139/…
@dzieciou 也很高兴在您使用 DI 容器时为您构建对象图,并且能够在一个地方将一种实现换成另一种实现也很高兴。通常对于愚蠢、简单的东西,我可能会传入依赖项,但在大多数框架中使用 DI 容器非常容易,通常也很容易做到这一点。
T
Trieu Toan

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

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

SomeClass() 的构造函数如下:

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

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

替代解决方案:

将 myObject 作为参数传递给构造函数

public SomeClass (MyClass myObject) {
    this.myObject = myObject;
}

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

一种常见的替代方法是定义一个不做任何事情的构造函数。依赖注入可以通过setter来完成。 (h/t @MikeVella)。

Martin Fowler 记录了第三种选择(h/t @MarcDix),其中类显式地为程序员希望注入的依赖项实现接口。

在没有依赖注入的情况下,很难在单元测试中隔离组件。

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


承认 Ben Hoffstein 对 Martin Fowler 文章的引用是必要的,因为它指出了该主题的“必读”,我接受 wds 的回答,因为它实际上回答了这里的问题。
+1 用于解释和动机:创建一个类依赖于其他人的问题的对象。另一种说法是 DI 使类更具凝聚力(它们的责任更少)。
您说依赖项是“传递给构造函数”的,但据我了解,这并不完全正确。如果在实例化对象后将依赖项设置为属性,它仍然是依赖项注入,对吗?
@MikeVella 是的,这是正确的。在大多数情况下,它没有真正的区别,尽管属性通常更灵活一些。我将稍微编辑文本以指出这一点。
到目前为止我找到的最好的答案之一,因此我真的有兴趣改进它。它缺少对依赖注入的第三种形式的描述:Interface injection
g
gtiwari333

我在 loose coupling 方面发现了这个有趣的例子:

来源:Understanding dependency injection

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

例如,考虑一个 Car 对象。

Car 取决于车轮、发动机、燃料、电池等来运行。传统上,我们将此类依赖对象的品牌与 Car 对象的定义一起定义。

没有依赖注入(DI):

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

  //The rest
}

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

如果我们想在最初的 NepaliRubberWheel() 穿孔之后更改其依赖对象的类型 - 比如 Wheel 怎么办?我们需要使用它的新依赖项重新创建 Car 对象,例如 ChineseRubberWheel(),但只有 Car 制造商可以这样做。

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

使用依赖注入时,对象在运行时而不是编译时(汽车制造时)被赋予其依赖关系。这样我们现在可以随时更改 Wheel。在这里,dependency (wheel) 可以在运行时注入 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、Battery)而不更改使用它的类(汽车)

提倡“代码接口而不是实现”原则

在测试期间创建和使用模拟依赖项的能力(如果我们想在测试期间使用车轮模拟而不是真实实例。我们可以创建模拟车轮对象并让 DI 框架注入到 Car)


我理解这一点的方式是,我们可以在需要时注入所述对象,从而消除第一个对象对它的依赖,而不是将新对象实例化为另一个对象的一部分。那正确吗?
我在此处以咖啡店示例对此进行了描述:digigene.com/design-patterns/dependency-injection-coffeeshop
真的很喜欢这个类比,因为它是简单的英语使用一个简单的类比。假设我是丰田,从设计到下线已经花费了太多的资金和人力来制造汽车,如果现有的知名轮胎生产商,我为什么要从头开始做一个轮胎制造部门,即{ 1}轮胎?我不。我所要做的就是从他们那里购买(通过参数注入),安装和哇啦!所以,回到编程,说一个C#项目需要使用一个现有的库/类,有两种方法可以运行/调试,1-添加对这个整个项目的引用
(续),.. 外部库/类,或从 DLL 中添加 2。除非我们必须查看这个外部类的内部内容,否则将其添加为 DLL 是一种更简单的方法。所以选项 1 是 new 它,选项 2 是作为参数传入。可能不准确,但简单愚蠢容易理解。
@JeliBeanMachine(很抱歉回复评论太晚了..)这不是我们删除第一个对象对轮对象或电池对象的依赖,而是我们将依赖传递给它,以便我们可以更改实例或实现依赖。之前:汽车对尼泊尔橡胶车轮有硬编码依赖。之后: Car 对 Wheel 的实例注入了依赖。
M
Mike

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

例如,考虑这些类:

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 上调用一些初始化“设置器”,代码增长得相当快,而 PersonService 现在依赖于不仅在 GroupMembershipService 上,而且在 GroupMembershipService 所依赖的所有其他东西上。此外,与 GroupMembershipService 的链接被硬编码到 PersonService 中,这意味着您不能“虚拟化”一个 GroupMembershipService 用于测试目的,或者在应用程序的不同部分使用策略模式。

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


如果您可以在使用 DI 之后给出相同的代码示例,那就太好了
“这也意味着任何作为 GroupMembershipService 的子类或实现 GroupMembershipService 接口的东西都可以“注入”到 PersonService 中,而 PersonService 不需要知道更改。” ...这对我来说是一个非常有用的提示 - 谢谢!
z
zby

公认的答案是一个很好的答案——但我想补充一点,DI 非常类似于在代码中避免硬编码常量的经典做法。

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

在面向对象编程的世界中,DI 与此类似。那里的值而不是常量文字是整个对象-但是将创建它们的代码从类代码中移出的原因是相似的-对象的更改比使用它们的代码更频繁。需要进行此类更改的一个重要案例是测试。


+1“对象比使用它们的代码更频繁地更改”。概括地说,在通量点添加一个间接。根据通量点,间接被称为不同的名称!
j
jcjr

让我们尝试使用 Car 和 Engine 类的简单示例,任何汽车都需要引擎才能去任何地方,至少现在是这样。下面是没有依赖注入的代码的外观。

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();

在这里,我们基本上将我们的依赖项(Engine 实例)注入(传递)到 Car 构造函数。所以现在我们的类在对象和它们的依赖之间有了松散的耦合,我们可以很容易地添加新类型的引擎而不改变 Car 类。

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

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

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

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

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


只是出于好奇,这与策略模式有何不同?这种模式封装了算法并使它们可互换。感觉依赖注入和策略模式非常相似。
这是一个很好的答案。
r
rgettman

假设你想去钓鱼:

如果没有依赖注入,您需要自己处理所有事情。你需要找一条船,买一根钓鱼竿,寻找诱饵,等等。当然有可能,但这会让你承担很多责任。用软件术语来说,这意味着您必须对所有这些东西进行查找。

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


另一方面,假设你雇了一个水管工来重修你的浴室,然后他说:“太好了,这是我需要你帮我拿来的工具和材料清单”。这不应该是水管工的工作吗?
所以有人需要照顾一些它不知道的人..但仍然决定收集船、棍子和诱饵的清单 - 尽管准备好使用。
@JoshCaswell 不,那将是水管工的雇主的工作。作为客户,您需要完成管道。为此,您需要一名水管工。水管工需要它的工具。为了得到这些,它由管道公司配备。作为客户,您不想确切地知道管道工做什么或需要什么。作为水管工,您知道自己需要什么,但您只想做好自己的工作,而不是得到一切。作为水管工的雇主,您有责任在将水管工送到人们家中之前,为他们配备他们需要的东西。
@kai 我明白你的意思。在软件中,我们谈论的是工厂,对吗?但是 DI 通常也意味着该类不使用工厂,因为它仍然没有被注入。您,客户,需要联系雇主(工厂)为您提供工具,以便您可以传递给管道工。这不是它在程序中的实际工作方式吗?因此,虽然客户(调用类/功能/其他)不必购买工具,但他们仍然必须是中间人,以确保他们从雇主(工厂)那里得到管道工(注入类)。
@KingOfAllTrades:当然在某些时候你必须有人雇用和装备水管工,或者你没有水管工。但是你没有客户这样做。客户只需要一名管道工,并得到一个已经配备了他工作所需的东西。使用 DI,您最终仍然有一些代码来满足依赖关系。但是您将它与真正起作用的代码分开。如果你把它发挥到极致,你的对象只会让它们的依赖关系为人所知,并且对象图的构建发生在外部,通常在 init 代码中。
P
Pmpr.ir

This是我见过的关于Dependency InjectionDependency Injection Container最简单的解释:

没有依赖注入

应用程序需要 Foo(例如控制器),所以:

应用程序创建 Foo

应用程序调用 Foo Foo 需要 Bar(例如服务),所以:Foo 创建 Bar Foo 调用 Bar Bar 需要 Bim(服务、存储库,...),所以:Bar 创建 Bim

Foo 需要 Bar(例如服务),所以:

Foo 创建 Bar

Foo 调用 Bar Bar 需要 Bim(服务、存储库……),所以: Bar 创建 Bim Bar 做某事

Bar 需要 Bim(服务、存储库……),所以:

Bar 创建 Bim

酒吧做点什么

依赖注入

应用需要Foo,需要Bar,需要Bim,所以:

应用程序创建 Bim

应用程序创建 Bar 并为其提供 Bim

应用程序创建 Foo 并给它 Bar

应用程序调用 Foo Foo 调用 Bar Bar 做某事

Foo 呼叫 Bar Bar 做某事

酒吧做点什么

使用依赖注入容器

应用程序需要 Foo 所以:

应用程序从 Container 中获取 Foo,因此: Container 创建 Bim Container 创建 Bar 并给它 Bim Container 创建 Foo 并给它 Bar

容器创建 Bim

Container 创建 Bar 并给它 Bim

Container 创建 Foo 并给它 Bar

应用程序调用 Foo Foo 调用 Bar Bar 做某事

Foo 呼叫 Bar Bar 做某事

酒吧做点什么

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

依赖注入是一种编写更好代码的方法

DI Container 是帮助注入依赖项的工具

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


恕我直言,最好的答案在这里,因为它没有将 ioc 或构造函数注入与 di 混合。
c
cherryblossom

在进行技术描述之前,首先用一个真实的例子来形象化它,因为你会发现很多技术资料来学习依赖注入,但大多数人无法理解它的核心概念。

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

您会觉得现在维护这个工厂的所有任务太复杂了,因为除了主要任务(在组装单元中组装汽车)您还必须专注于其他单元。现在维护成本非常高,而且厂房很大,所以要多花钱租。

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

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

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

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

https://i.stack.imgur.com/E7OWM.png

https://i.stack.imgur.com/auqDX.png


感谢这个概念的视觉表现!
T
TylerH

“依赖注入”不只是意味着使用参数化构造函数和公共设置器吗?

James Shore's article shows the following examples for comparison

没有依赖注入的构造函数: public class Example { private DatabaseThingie myDatabase; public Example() { myDatabase = new DatabaseThingie(); } 公共无效 doStuff() { ... myDatabase.getData(); ... } } 具有依赖注入的构造函数: public class Example { private DatabaseThingie myDatabase;公共示例(DatabaseThingie useThisDatabaseInstead){ myDatabase = useThisDatabaseInstead; } 公共无效 doStuff() { ... myDatabase.getData(); ... } }


当然,在 DI 版本中,您不想在无参数构造函数中初始化 myDatabase 对象吗?如果您尝试调用 DoStuff 而不调用重载的构造函数,似乎没有意义并且会引发异常?
仅当 new DatabaseThingie() 未生成有效的 myDatabase 实例时。
w
wakqasahmed

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

没有依赖注入

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

Switch -> PermanentBulb //switch 直接接永久灯泡,不易测试

https://i.stack.imgur.com/cIt97.jpg

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

依赖注入

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

Switch -> Bulb1 OR Bulb2 OR NightBulb(注入依赖)

https://i.stack.imgur.com/NrXaF.jpg

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(); 
    ... 
  } 
}`

C
Community

什么是依赖注入(DI)?

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

DI、DIP 和 SOLID

具体来说,在 Robert C Martin 的 SOLID principles of Object Oriented Design 范式中,DIDependency Inversion Principle (DIP) 的可能实现之一。 DIP is the D of the SOLID mantra - 其他 DIP 实现包括服务定位器和插件模式。

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# 实现。鉴于以下消费类:

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

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

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

public interface IClock
{
    DateTime Now { get; }
}

我们还可以将对 Console 的依赖放松为抽象,例如 TextWriter。依赖注入通常实现为 constructor 注入(将抽象传递给依赖项作为参数给消费类的构造函数)或 Setter Injection(通过 setXyz() 设置器或带有 { 的 .Net 属性传递依赖项) 6}定义)。构造函数注入是首选,因为这样可以保证类在构造后处于正确状态,并允许将内部依赖字段标记为 readonly (C#) 或 final (Java)。所以在上面的例子中使用构造函数注入,这给我们留下了:

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);
    }
}

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

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

[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());
}

下一步

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

每个抽象和配置的具体实现之间的映射(例如“任何时候消费者请求 IBar,返回一个 ConcreteBar 实例”)

可以为每个依赖项的生命周期管理设置策略,例如为每个消费者实例创建一个新对象,在所有消费者之间共享一个单例依赖实例,仅在同一个线程中共享相同的依赖实例等。

在 .Net 中,IoC 容器了解 IDisposable 等协议,并将承担根据配置的生命周期管理处理依赖项的责任。

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

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

根据上面的示例,解耦依赖项确实需要一些设计工作,对于开发人员来说,需要进行范式转变来打破直接new依赖项的习惯,转而信任容器来管理依赖项。

但是好处很多,特别是在彻底测试您感兴趣的课程的能力方面。

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


问题是 DIP != DI。 DIP 是关于从实现中解耦抽象: A. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。 B. 抽象不应该依赖于细节。细节应该取决于抽象。 DI 是一种将对象创建与对象使用分离的方法。
是的,在 Bob 叔叔的 SOLID 范式中,我的第 2 段 “DI 是 DIP 的可能实现之一” 清楚地说明了区别。我在之前的帖子中也做了这个clear
8
8 revs, 3 users 97%

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

清除依赖初始化代码

稳定,无论使用的依赖项如何

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

DI 的具体领域是依赖配置和初始化的委托。

示例:带有 shell 脚本的 DI

如果您偶尔在 Java 之外工作,请回想一下 source 如何经常用于许多脚本语言(Shell、Tcl 等,甚至在 Python 中被滥用于此目的的 import)。

考虑简单的 dependent.sh 脚本:

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

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

您在 archive_files_zip.sh 实施脚本中定义 archive_files(在本例中使用 zip):

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

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

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

archive_files dependency 刚刚被注入dependent 脚本中。

您可以使用 tarxz 注入实现 archive_files 的依赖项。

示例:删除 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 框架中那样被广泛强调和普及。

但这是一种解决以下问题的通用方法:

应用程序开发(单一源代码发布生命周期)

应用程序部署(具有独立生命周期的多个目标环境)

仅将配置与依赖项查找一起使用并没有帮助,因为配置参数的数量可能会根据依赖项(例如新的身份验证类型)以及受支持的依赖项类型的数量(例如新的数据库类型)而改变。


我将添加完成特定类(测试)而不必完成其依赖项的能力,作为 DI 的目的。
C
Community

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

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

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

https://i.stack.imgur.com/Ubcrh.jpg

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

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

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

我们可以将卷轴放在机器内,我们可以为卷轴提供一个挂钩,可以放置它。

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

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

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

高内聚和松耦合。

外化依赖,只看责任。

将事物作为组件并组合成一个功能强大的大型系统。

它有助于开发高质量的组件,因为它们是独立开发的,并且经过了适当的测试。

如果一个组件出现故障,它有助于用另一个组件替换该组件。

现在,这些概念构成了编程世界中众所周知的框架的基础。 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();
}

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

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();
}

你也可以阅读

Difference between Inversion of Control & Dependency Injection


I
Imam Bux

例如,我们有 2 个类 ClientServiceClient 将使用 Service

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();

优点

简单的

缺点

很难测试客户端类

当我们更改服务构造函数时,我们需要在所有地方更改代码创建服务对象

使用依赖注入

方式一)构造函数注入

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)Setter注入

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

===

现在,这段代码已经在 Dependency Injection 之后,并且更容易测试 Client 类。
但是,我们仍然多次使用 new Service(),并且在更改 Service 构造函数时并不好。为了防止它,我们可以使用 DI 注射器,如
1) 简单手动 Injector

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

优点

让测试更轻松

更改Service时,只需要在Injector类中更改即可

如果你使用构造函数注入,当你查看客户端的构造函数时,你会看到客户端类有多少依赖

缺点

如果你使用构造函数注入,Service对象是在创建Client时创建的,有时我们在Client类中使用函数而不使用Service,所以创建的Service是浪费的

依赖注入定义

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

依赖项是可以使用的对象(服务)注入是将依赖项(服务)传递给将使用它的依赖对象(客户端)


H
Harleen

什么是依赖注入?

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

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

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

控制反转 (IOC)

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

两种类型的依赖注入:

构造函数注入 Setter 注入

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 上使用基于注释而不是 @Required 注释可用于使 setter 成为必需的依赖项。


A
Anwar Husain

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

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

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

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

原因:因为您希望您的代码易于测试、可模拟等,以便您可以快速测试您的更改并将其推向市场。此外,当您有很多很棒的免费工具/框架来支持您在您拥有更多控制权的代码库的过程中,您为什么不这样做。


@WindRider 谢谢。我不能同意更多。人类的生命和人体是卓越设计的典范......脊柱是 ESB 的一个很好的例子 :)......
C
CubeJockey

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

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

实施 DI 有两种主要方法:

构造函数注入 Setter 注入

构造函数注入

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

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

二传手注入

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

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

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

以下是上述代码的示例:

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;

我认为您的第一段偏离了问题,根本不是 DI 的定义(即,您试图定义 SOLID,而不是 DI)。从技术上讲,即使你有 100 个依赖项,你仍然可以使用依赖注入。类似地,可以注入具体的依赖项——它仍然是依赖项注入。
K
Kevin Languasco

我知道已经有很多答案,但我发现这很有帮助: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) {...}
}

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


DI不会通过您已经构建的DataSourceImp接口传递给您吗?
d
drac_o

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

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

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

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

编辑

A nice comment by Jose Maria Arranz on DI

DI 通过消除确定依赖方向和编写任何胶水代码的任何需要来增加凝聚力。

错误的。依赖的方向是XML形式或注解,你的依赖被写成XML代码和注解。 XML 和注释是源代码。

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

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

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

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


P
Phil Goetz

流行的答案是无用的,因为它们以一种无用的方式定义依赖注入。让我们同意,“依赖”是指我们的对象 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 之间注入了依赖项。

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

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


N
Nikos M.

Dependency Injection 表示代码的一部分(例如类)访问依赖项(代码的其他部分,例如它所依赖的其他类)的一种方式(实际上是any-way)无需硬编码的模块化方式(因此它们可以自由更改或覆盖,甚至根据需要在其他时间加载)

(和 ps,是的,它已成为一个过于简单的 25 美元名称),我的 .25 美分


T
TastyCode

从书中,'Well-Grounded Java Developer: Vital techniques of Java 7 and polyglot programming

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


V
Volksman

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

不幸的是,有些人认为您需要一个专门的框架来进行依赖混淆,并且如果您选择不使用特定的框架来做这件事,那么您在某种程度上就是一个“小”程序员。许多人认为,另一个非常令人不安的神话是依赖注入是实现依赖混淆的唯一方法。从历史上看,这显然是 100% 错误的,但是您将难以说服某些人有依赖注入的替代方法来满足您的依赖混淆要求。

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


N
Nithin Prasad

5岁儿童的依赖注入。

当你自己去冰箱里取东西时,你可能会出问题。你可能会把门开着,你可能会得到一些妈妈或爸爸不希望你拥有的东西。您甚至可能正在寻找我们甚至没有或已过期的东西。

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


我不确定这种解释是否具有误导性,因为不利的服务定位器反模式似乎是所描述问题的有效解决方案。
M
Marius Waldal

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

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

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


B
BERGUIGA Mohamed Amine

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

依赖注入的目的是将解析外部软件组件的工作与应用程序业务逻辑分离。没有依赖注入,组件如何访问所需服务的细节可能会与组件的代码混淆。这不仅增加了错误的可能性,增加了代码膨胀,并放大了维护的复杂性;它将组件更紧密地耦合在一起,使得在重构或测试时很难修改依赖关系。


k
kusnaditjung tjung

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


h
hariprasad

依赖注入 (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 容器来完成的,它为我们完成了这项工作。我们只需在代码中创建上下文并调用 bean 的初始化。在那一刻,注射已自动完成。


a
adamw

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

依赖注入是创建服务对象的静态、无状态图的过程,其中每个服务都通过其依赖项进行参数化。

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

模块图——服务对象的图——通常在应用程序启动时创建。这可以使用容器来完成,例如 Spring,但也可以通过将参数传递给对象构造函数来手动完成。两种方式各有利弊,但在您的应用程序中使用 DI 绝对不需要框架。

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

什么时候使用?我会说,只要应用程序足够大,将逻辑封装到单独的模块中,模块之间的依赖图就会提高代码的可读性和可探索性。