1. 为什么要用依赖注入(DI)
什么是依赖注入,为什么要使用呢?简单通俗说就是一个类需要另一个类来协助工作,就产生了依赖,所以需要的依赖项就要【注入】过来一起来协同完成工作。
软件设计原则中有一个依赖倒置原则(DIP)讲的是要依赖于(1)抽象,不要依赖于具体,(2)高层模块不应该依赖于低层模块, 二者应该依赖于抽象。简单的说就是为了更好的解耦。而控制反转(Ioc)就是这样的一个实现思路, 这个思路的其中一种实现方式就是依赖注入(DI)。
感觉有点绕, 举个栗子:老李是一个维修工, 现在要出任务去维修, 得先去申领个扳手。
李: "请给我一把可以拧7mm大小的六角螺丝的扳手.", 然后库管老张就从仓库里拿了一把这样的大力牌扳手给老李。
在这个例子中, 维修工老李只要告诉库管我要一个 "可以拧7mm大小的六角螺丝"的扳手即可, 他不用关心扳手的品牌和样式, 也不用采购扳手,更不用关心这个扳手是怎么来的。
而对于库管, 他只需提供满足这样规则的一个扳手即可, 不用去关心老李拿着这个扳手之后去干什么。所以老李和老张都只是关心"可以拧7mm大小的六角螺丝的"这个规则即可, 也就是说, 如果后期仓库里不再提供大力牌扳手, 而是提供了这样的大牛牌扳手, 无论换了什么牌子和样式, 只要仍满足这个规则, 老李仍然可以正常工作。它们定义了一个规则(比如接口IWrench7mm), 二者都依赖于这个规则, 然后仓库无论提供大力牌(WrenchDaLi : IWrench7mm)还是大牛牌(WrenchDaNiu : IWrench7mm), 都不影响正常工作.
这就是依赖倒置原则(DIP), 不依赖于具体(牌子), 高层模块(老李)不应该依赖于低层模块(大力牌扳手), 二者应该依赖于抽象(IWrench7mm:可以拧7mm大小的六角螺丝)。如果直接由老李去获取(new)大力牌扳手, 那么当业务改变要求采用大牛牌的时候, 我们就要去修改老李的代码。为了解耦, 在本例中我们只要在配置中让仓库由原来的提供大力牌改为提供大牛牌即可。老李要使用的时候, 可以通过注入(构造器、属性、方法)的方式, 将仓库提供的扳手实例提供给老李使用。
注:仓库继承老李提出的接口(7mm规则)抽象方法,将其对接口方法进行实现
2. 依赖注入理解
引入依赖注入的目的是为了解耦。说白了就是面向接口编程,通过调用接口的方法,而不直接实例化对象去调用。
这样做的好处就是如果添加了另一个实现类,不需要修改之前代码,只需要修改注入的地方将实现类替换。上面说的通过接口调用方法,实际上还是需要去实例化接口的实现类,只不过不需要我们手动new 构造实现类,而是交给如微软的DI、Autofac这些工具去构建实现类。我们只需要告诉它们,某个类是某个接口的实现类,当用到的时候,工具(比如,微软的DI)会自动通过构造函数实例化类。
3. 依赖的服务如何注入
打开Startup这个文件, 看一下里面的ConfigureServices方法。顾名思义, 这个方法是用来配置服务,系统默认已经添加了一些服务, 剩下的就是我们把自己需要的用的添加进去。参数为服务集合IServiceCollection对象,这种对象提供了AddSingleton、AddScoped和AddTransient 三种方法来添加服务,三种方法添加的服务的生命周期不一样。
实例:
添加一个名为DIDemo的.NET CORE MVC项目,在该项目下创建一个服务文件夹(Servers)
1)定义接口ICount
(2)实现接口类Count
至此,服务(类)有了,那么如何能让这个服务为我们所用呢?或者说为我们服务呢?
(3)把类(服务)在Startup文件中通过ConfigureServices方法注入服务。
.NET Core 中自带的DI容器,可以理解为StartUp.CS文件。——不准确
使用容器的好处,由容器来统一管理实例的创建和销毁,你只需要关心怎么用就行了,不需要关系怎么创建跟销毁。
当然容器创建的实例都是有生命周期的。三种创建方法创建的实例生命周期不一样。
- Transient: 瞬态模式,每一次访问都会创建一个新的实例
- Scoped: 域模式,在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)。对象在一次请求中是相同的,但在不同请求中是不同的。
- Singleton :单例模式,整个应用程序生命周期以内只创建一个实例
此外,常用注入方式有三种。
public void ConfigureServices(IServiceCollection services)
{
……
//下面先以AddScopend方法阐述下常用的三种注入方式
//1.最常用的注入方式,以接口形式暴露服务。下面2中方式意思一样
//1.1 AddScopend后面是(),里面的接口和实现类必须套一层typeof
services.AddScoped(typeof(ICount), typeof(Count));
//1.2 AddScopend后面是<>,里面就直接写接口和实现类,当然最后有一个()
services.AddScoped<ICount, Count>();
//2.自己注入自己,以实现形式暴露服务
services.AddScoped(typeof(Count));
services.AddScoped<Count>();
//3.需要传参的构造函数的类的注入(后面实例有应用讲解)
// services.AddScoped(typeof(ICount), sp => { return new Count(参数); }) ;
//services.AddScoped<ICount>(sp => { return new Count(参数);}) ;
……
}
4)接下来分析演示三种注入方法的区别:
上面ConfigureServices方法中保留下面的瞬态模式
//第1种:瞬态模式,每一次访问都会创建一个新的实例
services.AddTransient<ICount, Count>();
服务注入之后,我们就要用它。切换到控制器。那么如何能把服务实例注入到控制器中来呢?有属性注入、构造方法注入、方法注入。这里一般会用构造方法注入
public class HomeController : Controller
{
private ICount _count;//方便本类其他方法的调用,所以定义一个私有字段来接收
public HomeController(ICount count)//通过构造方法注入实例,ASP.NET CORE内置了依赖注入容器
{
_count = count;
}
//说明:请求到home控制器,自然调用home控制器的构造方法,构造方法中需要一个ICount类型的对象,它怎么来的呢?这就是因为.NET Core内置了依赖注入容器,这个时候就会到StartUp.cs文件中的ConfigureServices方法中去找相应的依赖,而在那里告诉了ICount由Count来实现( services.AddTransient<ICount, Count>();),所以这时会去调用Count 的构造方法实例化Count对象。
//接下来就可以在控制器中使用_count
public IActionResult Index()
{
int c = _count.MyCount();
ViewBag.count = c;
return View();
}
前端展示
运行效果,不断刷新页面也总是0,因为瞬态模式注入的服务,每一次访问都会创建一个新的实例
上面ConfigureServices方法中改为下面的单例模式
//第2种:单例模式,整个应用程序生命周期以内只创建一个实例
services.AddSingleton<ICount, Count>();
运行效果,不断刷新页面不断增加1.
继续把上面ConfigureServices方法中改为下面的域模式
//第3种:域模式,在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
services.AddScoped<ICount, Count>();
运行效果,不断刷新页面一直保持为0,因为每次刷新页面都是一个新的请求,所以总是0,在一个请求内产生的实例对象才是唯一。
改进下测试代码
public class HomeController : Controller
{
private IServiceProvider _provider;
private ICount _count;//方便本类其他方法的调用,所以定义一个私有字段来接收
public HomeController(ICount count,IServiceProvider provider)//通过构造方法注入实例,ASP.NET CORE内置了依赖注入容器
{
_count = count;
_provider = provider;
}
//接下来就可以在控制器中使用_count
public IActionResult Index()
{
//int c = _count.MyCount();
//ViewBag.count = c;
//注意导入using Microsoft.Extensions.DependencyInjection;
ICount count1 = _provider.GetService<ICount>();
ICount count2 = _provider.GetService<ICount>();
int c1=count1.MyCount();
int c2 = count2.MyCount();
ViewBag.c1 = c1;
ViewBag.c2 = c2;
//ICount counter1 = _provider.GetService<ICount>();
//ICount counter2 = _provider.GetService<ICount>();
//int c1 = counter1.Get();
//int c2 = counter2.Get();
return View();
}
前端调整
测试发现c1、c2分别为0,1
但是每次刷新又重新为0,1
因为每次刷新页面都是一个新的请求,所以总是0,在一个请求内产生的实例对象才是唯一