用NetCore + ReactJS 实现一个前后端分离的网站 (2) 用依赖注入实现控制反转
1. 控制反转
刚接触控制反转的时候,颇有些挠头,它怎么就反转了呢。稍微熟悉了之后,才理解了一些。
假设有个方法定义在另一个工程里的某个类中,那么我们本来的做法就是引用这个工程,把那个类new出一个实例来,然后调用它的方法。
我们可以把这个方向(调用者->new->被调用者)称作正向。
那么,如果我们申明一个接口,然后用类去实现,并把这两个对象注册到一个容器中,让容器来管理类的实例化。当我们在任何地方需要用到这个接口提供的功能时,直接告诉容器说我要这个接口的具体实现,容器就会提供指定的类的实例,而不用自己new出一个来。
我们可以把这个方向(容器->new->被调用者->调用者)称作反向。
总之一句话,不用自己new实例了,而是别人new好了给你,就是控制反转。
2. 依赖注入
.NetCore默认支持依赖注入,使用也很简单,代码如下所示。在Program.cs中把服务(接口和类的关系)注册到内置的服务容器IServiceProvider中。
Program.cs
builder.Services.AddScoped<INovelService, NovelService>();
然后在NovelController.cs中通过构造函数注入服务,由容器负责创建和销毁实例。
有三种注入方式:构造函数注入、属性注入、方法注入
有三种生命周期:总是一个实例(Singleton)、一个请求一个实例(Scoped)、每次都是新实例(Transient)
这样NovelController.cs中就不需要再引用命名空间NovelTogether.Core.Service了,只需要引用NovelTogether.Core.IService,说明调用者现在只依赖接口,与实际提供功能的类解耦了。
NovelController.cs
using Microsoft.AspNetCore.Mvc;
using NovelTogether.Core.IService;
using NovelTogether.Core.Model;
//using NovelTogether.Core.Service;
namespace NovelTogether.Core.API.Controllers
{
[ApiController]
[Route("[controller]")]
public class NovelController : Controller
{
private readonly INovelService _novelService;
// 通过构造函数注入依赖
public NovelController(INovelService novelService)
{
_novelService = novelService;
}
[HttpGet]
public async Task<List<Novel>> Get()
{
// INovelService service = new NovelService();
// return await service.SelectAsync();
return await _novelService.SelectAsync();
}
}
}
同样的,服务层调用仓储层也可以使用依赖注入。
Program.cs
builder.Services.AddScoped<INovelRepository, NovelRepository>();
NovelService.cs
using NovelTogether.Core.IRepository;
using NovelTogether.Core.IService;
using NovelTogether.Core.Model;
//using NovelTogether.Core.Repository;
namespace NovelTogether.Core.Service
{
public class NovelService: INovelService
{
private readonly INovelRepository _novelRepository;
public NovelService(INovelRepository novelRepository)
{
_novelRepository = novelRepository;
}
public async Task<List<Novel>> SelectAsync()
{
//INovelRepository da = new NovelRepository();
//return await da.SelectAsync();
return await _novelRepository.SelectAsync();
}
}
}
到这里,除了API层之外,其余的工程就都只需要引用接口工程,而不需要引用具体的类工程了。引用的工作、或者说依赖关系都转移到了API层,因为需要在这里显式地注册依赖关系。
但这样有个问题,当项目越做越大,接口和类越来越多的时候,在Program.cs中手动添加依赖关系不是一个很优雅的方式,毕竟类已经继承了接口,如果可以识别他们之间关系,然后自动注入容器,不就很方便了吗?
于是,Autofac就登场了。
3. Autofac
Autofac是一个实现控制反转的容器,它的出现比NetCore默认支持的容器要早,功能也更丰富。
我们可以先替换内置的容器,来熟悉一下Autofac的使用。
3.1. 添加nuget包
3.2. 更换容器
Program.cs
//builder.Services.AddScoped<INovelRepository, NovelRepository>();
//builder.Services.AddScoped<INovelService, NovelService>();
#region Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
{
containerBuilder.RegisterType<NovelRepository>().As<INovelRepository>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NovelService>().As<INovelService>().InstancePerLifetimeScope();
}));
#endregion
下面是两种容器的生命周期对比
内置DI | Autofac |
---|---|
AddSingleton | SingleInstance |
AddScoped | InstancePerLifetimeScope |
AddTransient | InstancePerDependency |
3.3. 自动注册依赖
从程序执行目录中找到Service.dll和Repository.dll,并把其中所有的类以及依赖的接口注册到容器中。
Program.cs
//builder.Services.AddScoped<INovelRepository, NovelRepository>();
//builder.Services.AddScoped<INovelService, NovelService>();
#region Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
{
//containerBuilder.RegisterType<NovelRepository>().As<INovelRepository>().InstancePerLifetimeScope();
//containerBuilder.RegisterType<NovelService>().As<INovelService>().InstancePerLifetimeScope();
foreach (string assemblyPath in Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories))
{
if (new string[] { ".Service.dll", ".Repository.dll" }.Any(x => assemblyPath.EndsWith(x)))
{
var assembly = Assembly.LoadFile(assemblyPath);
containerBuilder.RegisterAssemblyTypes(assembly)
.AsImplementedInterfaces();
}
}
}));
#endregion
这样,再添加新的Service和Repository就不要在Program.cs中手动注册了。
目前,API层添加所有的工程依赖,如下图所示。
如果想要更优雅一些,可以删除Repository.dll和Service.dll,通过修改两个工程的Output路径,在编译的时候把dll文件输出到API的目录下面。
这个时候,API就只依赖接口以及Common、Model工程了。
是不是很漂亮,很符合强迫症患者的要求?
然而,这带来另一个问题,就是平常大家喜欢写完代码直接F5,编译+调试一气呵成。
但是,现在主程序和依赖工程脱节了,F5不会编译那些没添加依赖的工程,导致拿到了旧版本的Repository或者Service,于是还得执行Rebuild All,然后再F5。
不知道大家有没有什么好办法解决这个问题,这里我还是回到上个版本,显式地添加工程依赖项。
4. 结语
这一节介绍了.NetCore内置DI容器和autofac的简单使用,关于实例的生命周期的说明浅尝辄止,并没有展开来说。更加具体的细节问题,当遇到了再去参考大神们细致无比的文章,目前能够跑起来就满足需求了。
下一节要为仓储层添加一个基接口和基类,并实现数据库的接入。