我们需要依赖注入接口吗?
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无法控制。
(这并不意味着不要使用接口。这意味着您可以选择。)
我有一个 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无法控制。
(这并不意味着不要使用接口。这意味着您可以选择。)