我们需要依赖注入接口吗?

Do we need interfaces for dependency injection?

我有一个 ASP.NET 核心应用程序。该应用程序几乎没有完成某些工作的助手 classes。每个 class 都有不同的签名方法。我在网上看到很多 .net 核心示例,它们为每个 class 创建接口,然后向 DI 框架注册类型。例如

 public interface IStorage
 {
    Task Download(string file);
 }

 public class Storage
 {
    public Task Download(string file)
    {
    }
 }

 public interface IOcr
 {
     Task Process();
 }

 public class Ocr:IOcr
 {
    public Task Process()
    {

    }
 }

基本上每个接口只有一个class。然后我用 DI 将这些类型注册为

 services.AddScoped<IStorage, Storage>();
 services.AddScoped<IOcr,Ocr>();

但是我可以在没有接口的情况下注册类型,所以这里的接口看起来多余。例如

 services.AddScoped<Storage>();
 services.AddScoped<Ocr>();

所以我真的需要接口吗?

有用吗?是的。你应该这样做吗?没有。

依赖注入是依赖倒置原理的工具:https://en.wikipedia.org/wiki/Dependency_inversion_principle

或者如 SOLID

中所述

one should “depend upon abstractions, [not] concretions."

可以 只需在整个地方注入混凝土 类 就可以了。但这不是 DI 旨在实现的目标。

不,您不需要 依赖注入接口。但是依赖注入对它们更有用!

如您所见,您可以向服务集合注册具体类型,ASP.NET Core 会毫无问题地将它们注入您的 classes。通过注入它们而不是简单地使用 new Storage() 创建实例所获得的好处是 service lifetime management(瞬态 vs. 作用域 vs. 单例)。

这很有用,但只是使用 DI 的部分威力。正如@DavidG 所指出的,接口经常与 DI 配对的一个重要原因是因为测试。让您的消费者 classes 依赖于接口(抽象)而不是其他具体 classes 使它们更容易测试。

例如,您可以创建一个 MockStorage 来实现 IStorage 以便在测试期间使用,而您的消费者 class 应该无法分辨出其中的区别。或者,您可以使用模拟框架轻松地即时创建模拟的 IStorage。用具体的 classes 做同样的事情要困难得多。接口使得在不改变抽象的情况下替换实现变得容易。

我不会试图涵盖其他人已经提到的内容,使用带有 DI 的接口通常是最好的选择。但值得一提的是,有时使用对象继承可能会提供另一个有用的选择。例如:

public class Storage
 {
    public virtual Task Download(string file)
    {
    }
 }


public class DiskStorage: Storage
 {
    public override Task Download(string file)
    {
    }
 }

并像这样注册它:

services.AddScoped<Storage, DiskStorage>();

不,我们不需要界面。除了注入 classes 或接口之外,您还可以注入委托。相当于用一种方法注入一个接口。

示例:

public delegate int DoMathFunction(int value1, int value2);

public class DependsOnMathFunction
{
    private readonly DoMathFunction _doMath;

    public DependsOnAFunction(DoMathFunction doMath)
    {
        _doMath = doMath;
    }

    public int DoSomethingWithNumbers(int number1, int number2)
    {
        return _doMath(number1, number2);
    }
}

您可以在不声明委托的情况下执行此操作,只需注入一个 Func<Something, Whatever> 也可以。我倾向于委托,因为它更容易设置 DI。您可能有两个具有相同签名但服务于不相关目的的代表。

这样做的一个好处是它引导代码走向接口隔离。有人可能会想将方法添加到接口(及其实现)中,因为它已经被注入到某个地方所以很方便。

也就是说

  • 接口和实现承担了他们可能不应该承担的责任,只是因为它在当下对某些人来说很方便。
  • 依赖于接口的 class 也可以在其职责中增长,但更难识别,因为其依赖项的数量没有增长。
  • 其他 classes 最终取决于臃肿的 less-segregated 界面。

我见过这样的情况,其中单个依赖项最终会发展成实际上应该是两个或三个完全独立的 classes,所有这些都是因为添加到现有接口很方便 class而不是注入新的东西。这反过来又帮助一些 classes 达到了 2,500 行的长度。

你无法阻止某人做他们不该做的事。你不能阻止某人仅仅让 class 依赖于 10 个不同的代表。但它可以设定一种模式,引导未来朝着正确的方向发展,并为不断增长的界面提供一些阻力,class无法控制。

(这并不意味着不要使用接口。这意味着您可以选择。)