为什么使用依赖注入?

Why does one use dependency injection?

我试图理解依赖注入(DI),但我又一次失败了。只是看起来很傻。我的代码从来都不是一团糟;我几乎不写虚拟函数和接口(尽管我在蓝月时写过一次),我的所有配置都被神奇地序列化为使用json.net的类(有时使用XML序列化程序)。

我不太明白它能解决什么问题。它看起来像是一种表达方式:"嗨。当运行此函数时,返回此类型的对象并使用这些参数/数据。"但是…我为什么要用它?注意,我从来没有必要使用object,但我理解这是为了什么。

在构建一个使用DI的网站或桌面应用程序时,有哪些实际情况?我可以很容易地想出一些案例来解释为什么有人想要在游戏中使用界面/虚拟函数,但是在非游戏代码中使用界面/虚拟函数的情况非常罕见(非常罕见,我记不起单个实例)。


首先,我想解释一下我为这个答案所做的假设。这并不总是正确的,但经常是:好的。

Interfaces are adjectives; classes are nouns.

Ok.

(实际上,也有一些接口是名词,但我想在这里概括一下。)好的。

因此,例如,接口可以是诸如IDisposableIEnumerableIPrintable之类的东西。类是这些接口中的一个或多个的实际实现:ListMap都可以是IEnumerable的实现。好的。

要明白这一点:你的课程往往相互依赖。例如,您可以有一个访问您的数据库的Database类(哈哈,惊喜!;-)),但您还希望此类记录有关访问数据库的信息。假设您有另一个类Logger,那么Database依赖于Logger。好的。

到目前为止,一切都很好。好的。

您可以使用以下行在Database类中建模这种依赖关系:好的。

1
var logger = new Logger();

一切都很好。直到有一天,当你意识到你需要一堆日志记录程序时,这是很好的:有时你想登录到控制台,有时登录到文件系统,有时使用TCP/IP和远程日志服务器,等等…好的。

当然,你不想改变你所有的代码(同时你有无数的代码)和替换所有的行。好的。

1
var logger = new Logger();

通过:好的。

1
var logger = new TcpLogger();

首先,这不好玩。其次,这很容易出错。第三,对于一只训练有素的猴子来说,这是愚蠢的、重复的工作。那你怎么办?好的。

显然,引入由所有不同的记录器实现的接口EDOCX1(或类似接口)是一个很好的主意。所以代码的第一步是:好的。

1
ICanLog logger = new Logger();

现在类型推断不再改变类型,您总是有一个单独的接口来开发。下一步是你不想让new Logger()一次又一次地重复。因此,您将可靠性设置为向单个中心工厂类创建新实例,并获得以下代码:好的。

1
ICanLog logger = LoggerFactory.Create();

工厂本身决定要创建哪种记录器。你的代码不再关心了,如果你想改变正在使用的记录器的类型,你只需要在工厂里改变它一次。好的。

现在,当然,您可以概括这个工厂,使它适用于任何类型:好的。

1
ICanLog logger = TypeFactory.Create<ICanLog>();

在某个地方,当请求特定接口类型时,这个类型工厂需要实际类实例化的配置数据,因此您需要一个映射。当然,您可以在代码中进行这种映射,但是类型更改意味着重新编译。但是您也可以将这个映射放在一个XML文件中,例如……这允许您更改实际使用的类,即使是在编译时!!),这意味着动态的,不需要重新编译!好的。

为您提供一个有用的例子:考虑一个不正常记录的软件,但是当您的客户因为有问题而打电话寻求帮助时,您发送给他的只是一个更新的XML配置文件,现在他已经启用了记录功能,您的支持可以使用日志文件来帮助您的客户。好的。

现在,当您稍微替换一点名称时,您会得到一个服务定位器的简单实现,这是控制反转的两种模式之一(因为您反转了控制权,决定了要实例化的确切类是谁)。好的。

总之,这减少了代码中的依赖性,但现在所有代码都依赖于中心的单一服务定位器。好的。

依赖注入现在是这一行中的下一步:只需去掉对服务定位器的这个单一依赖:您不再要求服务定位器实现特定接口的各种类,而是再次恢复对谁实例化什么的控制。好的。

通过依赖注入,您的Database类现在有了一个构造函数,它需要ICanLog类型的参数:好的。

1
public Database(ICanLog logger) { ... }

现在,您的数据库总是有一个日志记录器可供使用,但它不再知道这个日志记录器来自何处。好的。

这就是DI框架发挥作用的地方:您再次配置映射,然后让DI框架为您实例化应用程序。由于Application类需要ICanPersistData实现,因此会注入Database的一个实例,但为此,它必须首先创建一个为ICanLog配置的记录器类型的实例。等等…好的。

因此,简而言之:依赖注入是如何删除代码中依赖项的两种方法之一。它对于编译后的配置更改非常有用,对于单元测试来说是一件很好的事情(因为它使得注入存根和/或模拟非常容易)。好的。

在实践中,如果没有服务定位器(例如,如果您事先不知道您需要多少特定接口的实例:DI框架总是每个参数只注入一个实例,但是您当然可以在循环中调用服务定位器),因此通常每个DI框架也提供一个服务L。欧克托好的。

但基本上就是这样。好的。

希望有帮助。好的。

PS:我在这里描述的是一种称为构造函数注入的技术,还有一种属性注入,它不是构造函数参数,而是用于定义和解析依赖项的属性。将属性注入视为可选依赖项,将构造函数注入视为强制依赖项。但关于这一点的讨论超出了这个问题的范围。好的。好啊。


我认为很多时候人们会对依赖注入和依赖注入框架(或者通常称为容器)之间的区别感到困惑。

依赖注入是一个非常简单的概念。代替此代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

您编写的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no"new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

就是这样。说真的。这给了你很多好处。两个重要的功能是从一个中心位置(Main()功能)控制功能,而不是在整个程序中传播它,以及更容易单独测试每个类的能力(因为可以将模拟或其他伪造对象传递到其构造函数中而不是实际值)。

当然,缺点是您现在有了一个大型函数,它知道程序使用的所有类。这就是DI框架可以帮助解决的问题。但是,如果您无法理解这种方法的价值所在,我建议首先从手动依赖注入开始,这样您就可以更好地了解现有的各种框架可以为您做些什么。


如其他答案所述,依赖注入是在使用它的类之外创建依赖项的一种方法。你从外部注入它们,从你的类内部控制它们的创建。这也是依赖注入实现控制反转(IOC)原理的原因。

国际奥委会是原则,DI是模式。就我的经验而言,你可能"需要不止一个记录器"的原因从未真正得到满足,但真正的原因是,每当你测试某个东西时,你确实需要它。一个例子:

我的特点:

When I look at an offer, I want to mark that I looked at it automatically, so that I don't forget to do so.

您可以这样测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

所以在OfferWeasel中的某个地方,它构建了一个这样的报价对象:

1
2
3
4
5
6
7
8
9
public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

这里的问题是,这个测试很可能总是失败的,因为被设置的日期将不同于被断言的日期,即使您只是在测试代码中放入DateTime.Now,它可能会关闭几毫秒,因此总是失败。现在更好的解决方案是为此创建一个接口,允许您控制将设置的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

接口是抽象的。一个是真的,另一个允许你在需要的时候伪造一些时间。然后可以这样更改测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

像这样,通过注入依赖项(获取当前时间),您应用了"控制反转"原则。这样做的主要原因是为了更容易地进行隔离单元测试,还有其他的方法。例如,这里不需要一个接口和一个类,因为在C函数中可以作为变量传递,所以您可以使用Func来实现相同的接口,而不是接口。或者,如果采用动态方法,只需传递具有等效方法(duck类型)的任何对象,而根本不需要接口。

你几乎不需要不止一个伐木工人。尽管如此,依赖注入对于静态类型代码(如Java或C语言)来说是必不可少的。

还有…还应该注意的是,如果对象的所有依赖项都可用,那么它只能在运行时正确地实现其目的,因此在设置属性注入时没有太多用途。在我看来,当调用构造函数时,所有依赖项都应该得到满足,所以构造函数注入是必须的。

希望能帮上忙。


我认为经典的解决方案是创建一个更加分离的应用程序,它不知道运行时将使用哪个实现。

例如,我们是中央支付提供商,与世界各地的许多支付提供商合作。但是,当发出请求时,我不知道要呼叫哪个支付处理器。我可以用大量的交换案例编写一个类,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

现在假设您需要在一个类中维护所有这些代码,因为它没有正确地解耦,您可以想象对于您将支持的每个新处理器,您需要为每个方法创建一个新的if//switch case,但是,通过使用依赖注入(或控制反转,因为它是SOM),这只会变得更复杂。调用的时间,意味着控制程序运行的人只能在运行时知道,而不是复杂的情况),您可以实现非常整洁和可维护的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

**代码不会编译,我知道:)


使用DI的主要原因是,您希望将知识的责任放在知识所在的实现中。DI的思想与封装和接口设计非常一致。如果前端从后端请求一些数据,那么前端如何解决该问题不重要吗?这由请求处理程序决定。

这在OOP中已经很常见了很长一段时间。多次创建代码片段,如:

1
I_Dosomething x = new Impl_Dosomething();

缺点是实现类仍然是硬编码的,因此前端具有使用哪个实现的知识。DI将接口设计进一步,前端只需要知道接口的知识。在dyi和di之间是服务定位器的模式,因为前端必须提供一个键(存在于服务定位器的注册表中),以使其请求得到解决。服务定位器示例:

1
I_Dosomething x = ServiceLocator.returnDoing(String pKey);

实例:

1
I_Dosomething x = DIContainer.returnThat();

DI的一个要求是容器必须能够找出哪个类是哪个接口的实现。因此,DI容器是否需要强类型设计,并且每个接口同时只需要一个实现。如果同时需要更多接口实现(如计算器),则需要服务定位器或工厂设计模式。

D(B)I:依赖注入和接口设计。然而,这种限制并不是一个非常大的实际问题。使用D(B)i的好处是它可以为客户和提供者之间的通信提供服务。界面是一个对象或一组行为的透视图。后者在这里至关重要。

我更喜欢在编码方面与D(B)I一起管理服务合同。他们应该一起去。在我看来,将D(B)i作为一个没有服务合同组织管理的技术解决方案的使用并不是非常有益的,因为DI只是一个额外的封装层。但是,当您可以将它与组织管理一起使用时,您可以真正利用我提供的组织原则D(B)。从长远来看,它可以帮助您在测试、版本控制和替代方案开发等主题上与客户机和其他技术部门进行结构通信。当你像在硬编码类中那样有一个隐式接口时,那么随着时间的推移,它的可沟通性会大大降低吗?当你使用d(b)i显式接口时,它的可沟通性会大大降低。这一切归根结底都归结为维护,这是一段时间,而不是一个时间。-)