之前记录的依赖注入太复杂,今天认真看了《C#高级编程》里面对依赖注入的解释,明显就简单很多。
1、什么是依赖注入?为什么需要它?
更快的开发周期需要单元测试和更好的可更新性。更改一些代码,不应该导致意外位罝出现错误。创建更模块化的、减少依赖项的应用程序,有助于防止这种错误。
依赖注入(Dependency Injection,DI)允许从类的外部注入依赖项,因此注入依赖项的类只需要知道一个协定(通常是C#接口)。这个类可以独立于其对象的创建。
依赖注入更便于进行单元测试。在单元测试中,只需要测试特定的类,需要的依赖项可以替换为包含测试数据的特殊模拟类。
还可以使用不同的实现区分生产模式和开发模式。例如,在生产过程中,可能需要访问SAP服务器,或者可能需要对所有开发人员都无法访问的特定活动目录进行身份验证。在开发的每个调试会话期间,都不希望等待成功的身份验证,也不需要SAP服务器开发用户界面。在这里,可以给相同的协定使用不同的实现来模拟身份验证,可以使用测试数据而不是访问SAP服务器。
也可以在不同的平台上使用不同的实现。例如,可以创建一个.NET标准库,在其中为UWP、WPF和Xamarin应用程序实现所有公共功能,并可以根据需要重定向到特定于平台的代码。
依赖注入还允许用自定义特性替换标准功能。ASP.NETCore和EntityFrameworkCore主要基于依赖注入。这些技术使用数百个协定一例如,来找到控制器,将HTTP请求映射到控制器,将接收到的数据转换为参数,将数据库表映射到实体类型等。使用不同的实现,可以轻松地替换自定义功能。
DI是敏捷软件开发和持续软件交付实践的核心模式。
依赖注入不需要依赖注入容器,但该容器有助于管理依赖项。依赖注入容器管理的服务列表越来越长,就可以看到它的优点。ASP.NETCore和EntityFrameworkCore使用Microsoft.Exteosions.DependencyInjection作为容器来管理所有依赖项,以此管理数百个服务。
尽管依赖注入和依赖注入容器在非常小的应用程序中会增加复杂性,但是一旦应用程序变得更大,需要多个服务,依赖注入就会降低复杂性,并促进非紧密绑定的实现。
2、没有依赖注入
下面的示例没有使用依赖注入;稍后将更改它,以使用依赖注入。所用的服务实现在类GreetingService中定义。这个类定义了返回字符串的Greet方法:
public class GreetingService
{
public string Greet(stringname)=>$"Hello,{name}";
}
类HomeController使用这个服务。在Hello方法中,实例化了GreetingService,并且调用Greet方法:
public class HomeController
{
public string Hello(string name)
{
var service = new GreetingService();
return service.Greet(name);
}
}
下面看看Program类的Main()方法。其中实例化了HomeController,调用Hello方法,将结果写入控制台:
static void Main()
{
var controller=new HomeController();
string result=controller.Hello("Stephanie");
Console.WriteLine(result);
}
程序运行时,把Hello,Stephanie写入控制台。这有什么问题吗?
HomeController和GreetingService是紧密稱合的。要用不同的实现取代HomeController中的GreetingService并不容易。这个GreetingService是一个返回字符串的简单服务。在正常的应用程序中,场景通常更复杂。例如,GreetingService可能使用HTTP请求访问API服务,或者使用EntityFramewoik访问数据库。可能要更改在一个地方使用的服务,而不是査找使用服务的所有位置。
另外,为HomeController创建单元测试时,也会测试GreetingService。在单元测试中,希望仅测试单个类的方法的功能,而不需要使用其他依赖项。在HomeController中,不能很容易地为单元测试替换GreetingService。从技术上讲,为单元测试替换GreetingService方法的内部实现是可能的。使用Microsoft Fakes框架,可以通过替换GreetingSeivice类的特定方法和属性,来更改方法的实现。这个变更是在单元测试中定义的,并且只有在单元测试运行时才会发生:通过另一个方法来“伪造”原来的方法。其实这有更好的方法:使用依赖注入。
下一节将介绍如何更改此实现,以使用依赖注入。
3、使用依赖注入实现
下面使HomeController独立于GreetingService的实现。为此,可以创建接口IGreetingService,它定义了HomeController所需的功能:
public interface IGreetingService
{
string Greet(stringname);
}
GreetingService现在实现了接口IGreetingService:
public class GreetingService:IGreetingService
{
public string Greet(stringname)=>$"Hello,{name}";
}
HomeController现在只需要对一个对象的引用,该对象实现了接口IGreetingService。它用HomeController的构造函数注入,分配给私有字段,通过方法Hello来使用:
public class HomeController
{
private readonly IGreetingService _greetingService;
public HomeController(IGreetingService greetingService)
{
_greetingService=greetingService??
throw new ArgumentMullException(nameof(greetingService));
}
public string Hello(stringname) => _greetingService.Greet(name);
}
在这个实现中,HomeController利用了控制反转的设计原理。HomeController没有像以前那样实例化GreetingService。相反,定义由HomeController使用的具体类的控件在外部给出;换句话说,控制是反转的。
注意:控制反转也被称为好莱坞原則:不要给我们打电话;我们会给你打电话。
控制反转也减少了对不同技术的依赖,创建出更通用的代码。例如,可以在.NET标准库中为WPF、UWP和Xamarin应用程序一同使用相同的视图糢型和服务协定。对于WPF、UWP和Xamarin,有些服务需要不同的实现。这种服务的实现可以来自于托管应用程序,而协定是在.NET标准库中定义和使用的。阅读第34章可以获得关于视图模型的更多信息。
类HomeController并没有依赖IGreetingService接口的具体实现。
HomeController可以使用实现了接口IGreetingService的任何类。这个类只需要实现这个接口的所有成员。现在,需要从外部注入依赖项,将具体的实现传递给HdloControUer类的构造函数。在样例代码中,使用构造函数注入的依赖注入模式实现了控制反转的设计原则。它称为构造函数注入,因为接口是在构造函数中注入的。需要注入依赖项来创建一个HomeController实例。
下面修改Main()方法,将IGreetingService的具体实现传递给HomeControIler。这里,注入了依赖项:
static void Main()
{
var controller=new HomeController(new GreetingService());
string result=controller.Hello("Matthias");
Console.WriteLine(result);
}
为HomeController的Hello方法创建一个单元测试,可以注入不同的实现。例如,执行IGreetingService的MockGreetingService。
示例应用程序目前非常小。唯一需要注入的是一个实现协定的具体类。这个类在实例化的同时实例化了HomeController。在实际应用程序中,需要处理许多接口和实现,还需要共享实例。这样做的一个简单方法是使用依赖注入容器来管理所有依赖项。应用程序还可以使用Microsoft.Extensions.Dependencylnjection容器。或者使用第三方的依赖注入容器,比如Autofac。