用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();
        }
    }
}

到这里,除了API层之外,其余的工程就都只需要引用接口工程,而不需要引用具体的类工程了。
但这样有个问题,当项目越做越大,接口和类越来越多的时候,在Program.cs中手动添加接口和实体类的关系不是一个很优雅的方式,毕竟类已经继承了接口,如果可以识别他们之间关系,然后自动注册到容器中,不就很方便了吗?
于是,Autofac就登场了。

3. Autofac

Autofac是一个实现控制反转的容器,它的出现比NetCore默认支持的容器要早,功能也更丰富。
我们可以先替换内置的容器,来熟悉一下Autofac的使用。

3.1. 添加nuget包

image

3.2. 更换容器

Program.cs
//builder.Services.AddScoped<INovelService, NovelService>();

#region Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
{
    containerBuilder.RegisterType<NovelService>().As<INovelService>().InstancePerLifetimeScope();
}));
#endregion

下面是两种容器的生命周期对比

内置DI Autofac
AddSingleton SingleInstance
AddScoped InstancePerLifetimeScope
AddTransient InstancePerDependency

3.3. 自动注册接口和类

从程序执行目录中找到Service.dll,并把其中所有的类以及相应的接口注册到容器中。

Program.cs
//builder.Services.AddScoped<INovelService, NovelService>();

#region Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
{
    //containerBuilder.RegisterType<NovelService>().As<INovelService>().InstancePerLifetimeScope();

    foreach (string assemblyPath in Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories))
    {
        if (new string[] { ".Service.dll" }.Any(x => assemblyPath.EndsWith(x)))
        {
            var assembly = Assembly.LoadFile(assemblyPath);
            containerBuilder.RegisterAssemblyTypes(assembly)
                .AsImplementedInterfaces();
        }
    }
}));
#endregion

这样,再添加新的Service就不要在Program.cs中手动注册了。
目前,API层添加所有需要引用的工程,如下图所示。

image

如果想要更优雅一些,可以删除Service.dll,通过修改Service工程的Output路径,在编译的时候把dll文件输出到API的目录下面。
image

这个时候,API就只需要引用接口以及Common、Model工程了。
image

是不是很漂亮,很符合强迫症患者的要求?
然而,这带来另一个问题,就是平常大家喜欢写完代码直接F5,编译+调试一气呵成。
但是,现在主程序和依赖工程脱节了,F5不会编译那些没添加的工程,导致拿到了旧版本的Service,于是还得先F6,然后再F5。
不知道大家有没有什么好办法解决这个问题,这里我还是回到上个版本,显式地添加工程。

4. 结语

这一节介绍了.NetCore内置DI容器和autofac的简单使用,关于实例的生命周期的说明浅尝辄止,并没有展开来说。更加具体的细节问题,当遇到了再去参考大神们细致无比的文章,目前能够跑起来就满足需求了。
下一节要为服务层添加一个基接口和基类,并实现数据库的接入。

posted @ 2022-11-21 14:43  王一乙  阅读(247)  评论(0编辑  收藏  举报