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

Posted

技术标签:

【中文标题】我们需要依赖注入的接口吗?【英文标题】:Do we need interfaces for dependency injection? 【发布时间】:2017-03-28 20:25:25 【问题描述】:

我有一个 ASP.NET Core 应用程序。该应用程序有几个帮助类可以做一些工作。每个类都有不同的签名方法。我在网上看到很多 .net 核心示例,它们为每个类创建接口,然后使用 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()
    

    
 

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

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

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

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

那么我真的需要接口吗?

【问题讨论】:

临时锁,让人们在cmets冷静下来。 【参考方案1】:

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

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

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

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

【讨论】:

很好的答案。除了测试接口的好处之外,我想补充一点,接口还允许拦截依赖项,例如使用装饰器设计模式。例如,可以将其他行为添加到IStorage,而无需更改Storage 实现本身。例如,您可能希望记录每个下载的文件及其大小,或阻止某些用户下载某些文件。【参考方案2】:

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

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

或者正如SOLID中描述的那样

一个人应该“依赖于抽象,[而不是]具体化。”

可以在各处注入具体的类,它就可以工作。但这不是 DI 旨在实现的目标。

【讨论】:

但是,如果您使用的是其他人创建的某些“组件”(没有接口),您可能别无选择,对吧? 在这种情况下,您可以为该组件创建一个接口。应该是一个 ctrl + 。离开 依赖注入不是依赖倒置的工具。这两个概念经常齐头并进(这是正确的),但你可以在没有另一个的情况下完成任何一个。【参考方案3】:

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

public class Storage
 
    public virtual Task Download(string file)
    
    
 


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

并像这样注册它:

services.AddScoped<Storage, DiskStorage>();

【讨论】:

但它只对抽象类有用,对吧?您的 Storage 类没有用 abstract 关键字修饰。它包含虚拟方法 - 虚拟方法必须具有默认实现,而不是抽象方法。 不,好问题。您可以将 Storage 设为 Abstract 类,在这种情况下,您无需在该类中提供 Download 的实现。但是 Storage 可以是一个常规的 ol' 类,如我所示,它可以使用 virtual for 方法,可以被覆盖,如图所示。将方法声明为 virtual 向编译器表明允许派生类提供它自己的方法实现。您可以在这里了解更多信息:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…【参考方案4】:

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

例子:

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&lt;Something, Whatever&gt;,这也可以。我倾向于代表,因为它更容易设置 DI。您可能有两个具有相同签名但服务于不相关目的的委托。

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

意思是

接口和实现承担了他们可能不应该承担的责任,因为这对当时的某人来说很方便。 依赖于接口的类的职责也会增加,但由于其依赖项的数量没有增加,因此更难识别。 其他类最终取决于臃肿、隔离程度较低的接口。

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

你不能阻止某人做他们不应该做的事。你不能阻止某人仅仅让一个班级依赖于 10 个不同的代表。但它可以设置一种模式,引导未来朝着正确的方向发展,并为不断增长的接口和类提供一些阻力。

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

【讨论】:

以上是关于我们需要依赖注入的接口吗?的主要内容,如果未能解决你的问题,请参考以下文章

spring 依赖注入详解

angularjs广播需要注入吗

Spring的依赖注入(DI)三种方式

依赖注入(DI)和Ninject,Ninject

PHP实现依赖注入

依赖注入