什么是依赖注入?

已经发布了几个问题,其中包含有关依赖项注入的特定问题,例如何时使用它以及支持它的框架。然而,

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

答案

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

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

Martin Fowler 的一篇文章可能也很有用。

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

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

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

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

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 的一个主要主题。这对我来说仍然是最大的优势,因为程序员在运行时设计中并不总是需要额外的灵活性(例如,对于服务定位器或类似的模式)。程序员经常需要在测试期间隔离类。

我从松耦合的角度发现了这个有趣的例子:

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

例如,考虑一个Car对象。

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中。

使用依赖项注入后:

在这里,我们在运行时注入 依赖项 (车轮和电池)。因此,术语: 依赖注入。

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

资料来源: 了解依赖注入

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

例如,考虑以下情况:

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 ,以完成其工作。如果没有依赖注入,这样做的传统方式是实例化一个新GroupMembershipService中的构造PersonService和使用实例属性中的两种功能。但是,如果GroupMembershipService的构造函数有很多要求,或者更糟糕的是,则需要在GroupMembershipService上调用一些初始化 “setter”,代码会快速增长,并且PersonService现在不仅依赖于GroupMembershipService而且取决于还有GroupMembershipService依赖的其他所有内容。此外,与GroupMembershipService的链接被硬编码到PersonService ,这意味着您不能 “ GroupMembershipServiceGroupMembershipService进行测试,也不能在应用程序的不同部分中使用策略模式。

依赖注入,而不是实例化GroupMembershipService你的内PersonService ,你要么把它传递到PersonService构造,否则添加属性(getter 和 setter)来设置它的本地实例。这意味着您的PersonService不再需要担心如何创建GroupMembershipService ,而只需接受它所提供的服务并与之一起使用。这也意味着可以将任何属于GroupMembershipService的子类或实现GroupMembershipService接口的东西 “注入” 到PersonService ,而PersonService不需要知道更改。

可接受的答案是一个很好的答案 - 但我想补充一点,DI 非常类似于经典避免代码中的硬编码常量。

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

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

让我们尝试一个有关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();

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

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

因此,最终依赖注入只是一种用于实现对象及其依赖之间的松散耦合的技术。与其直接实例化类执行其动作所需的依赖关系,不如(通常)通过构造函数注入将依赖关系提供给该类。

同样,当我们有很多依赖项时,最好使用 Inversion of Control(IoC)容器,该容器可以告诉我们所有依赖项应映射到哪个具体实现的接口,并且可以在构造时为我们解决这些依赖项我们的对象。例如,我们可以在 IoC 容器的映射中指定IEngine依赖项应映射到GasEngine类,当我们向 IoC 容器询问Car类的实例时,它将自动构造具有GasEngine依赖项的Car类通过了。

更新:最近观看了朱莉 · 勒曼(Julie Lerman)的有关 EF Core 的课程,也喜欢她关于 DI 的简短定义。

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

假设您想去钓鱼:

  • 如果没有依赖项注入,则您需要自己做好一切。您需要找到一条船,购买一根钓鱼竿,寻找诱饵等。当然有可能,但是这给您带来了很多责任。用软件术语,这意味着您必须对所有这些内容执行查找。

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

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

没有依赖注入

  • 应用程序需要 Foo(例如,控制器),因此:
  • 应用程序创建 Foo
  • 应用程序调用 Foo
    • Foo 需要 Bar(例如服务),因此:
    • Foo 创建栏
    • Foo 电话酒吧
      • Bar 需要 Bim(服务,存储库等),因此:
      • 栏创建 Bim
      • 酒吧做什么

有依赖注入

  • 应用程序需要 Foo,需要 Bar,需要 Bim,因此:
  • 应用程序创建 Bim
  • 应用程序创建 Bar 并将其赋予 Bim
  • 应用程序创建 Foo 并将其赋予 Bar
  • 应用程序调用 Foo
    • Foo 电话酒吧
      • 酒吧做什么

使用依赖注入容器

  • 应用程序需要 Foo,因此:
  • 应用程序从容器中获取 Foo,因此:
    • 容器创建 Bim
    • 容器创建条并将其赋予 Bim
    • 容器创建 Foo 并赋予它 Bar
  • 应用程序调用 Foo
    • Foo 电话酒吧
      • 酒吧做什么

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

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

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

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

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

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

没有依赖注入

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

开关 -> PermanentBulb // 开关直接连接到永久灯泡,无法轻松测试

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

有依赖注入

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

开关 -> Bulb1 OR Bulb2 OR 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(); 
    ... 
  } 
}`