依赖注入 DI

前言

声明:我是一个菜鸟,此文是自己的理解,可能正确,可能有误。仅供学习参考帮助理解,如果直接拷贝代码使用造成损失概不负责。

相关的文章很多,我就仅在代码层面描述我所理解的依赖注入是个什么,以及在 .Net 开发中如何使用。以下可能出现的词汇描述:

  • IoC:Inversion of Control,控制反转
  • DI:Dependency Injection,依赖注入

什么是依赖注入?

  • IoC 是一种设计,属于思想,而 DI 是实现这个设计的一种手段

  • 依赖注入是编码中为了减少写死的依赖,从而实现 IoC。

百度百科描述:控制反转_百度百科 (baidu.com)

传统做法

可能出现类似下方的代码
public class OrderService
{
public OrderResult Order(long userId, long productId, long quantity)
{
// 实例化用户服务
// var userService = new UserService();
// 查询用户信息
// userService.GetUserInfo(userId); // 实例化产品服务查询产品信息
// var productService = new ProductService();
// 查询用户信息
// productService.GetProductInfo(userId); // 实例化订单仓储,写数据入库
// var orderRepository = new OrderRepository();
// 下单
// orderRepository.Save(......); // 省略
}
} public class OrderResult { }
  • 问题
    • 在具体的业务功能方法里创建服务实例
    • 在业务方法里,使用new关键字创建了一个其他业务的实例。
  • 思考,要达到解耦的目的,把实现类处理成上端配置。
    • 我们需要一个工具帮我们创建这个对象,而且还要求代码告诉工具需要什么东西,但是不能把这个服务类型写死。
    • 接口就是这个用于告诉服务提供器所需服务到底是什么的一个暗号,服务提供器会根据配置,把所需的服务类型构造好。
  • 由上面的描述,可以知道这个帮我们构造对象的东西有几个要素:
    • 容器:需要有个地方存放配置
    • 注册:需要有一个键值对用来指定抽象和实现的关系
    • 服务提供器:光是知道什么类型并不够,构造对象的这个过程需要考虑逐级依赖比较复杂,所以还需要一个提供器代劳。
  • 依赖注入是什么

    通过上面描述的这样一个工具,达到一个使用者和具体实现类型解耦这样的目的,这个过程就是依赖注入。

依赖注入有什么优点?

  • 依赖注入可以让当前服务和其使用到的服务实现没有耦合(解耦)

  • 构造具体的服务时,不需要操心该服务的细节配置(不关注细节)

  • 入口往容器注册一次之后,业务代码中可多次注入使用(复用)

    由上可知,达到这个目标之后,细节的配置不在下端,而是在上端进行,实现控制的反转 IoC。

Net 自带的 IServiceCollection 如何使用

上面都是一些自己对概念的理解。可能看起来仍然很抽象。此处演示一下 .Net 自带的依赖注入容器 ServiceCollection 如何简单使用。

  1. 控制台程序/Winform

    // 定义一个容器(可以理解为字典)
    var services = new ServiceCollection(); // 注册服务:添加键值对到字典中存放
    services.AddTransient<ITestService, TestService>(); // TestService的构造函数有一个IUserService入参
    services.AddTransient<IUserService, UserService>();
    services.AddTransient<ITest001Service, Test001Service>(); // 创建一个服务提供器
    var povider = services.BuildServiceProvider(); // 获取服务:根据Key从字典中获取到想要的类型
    var service = povider.GetService<ITestService>(); // 但是使用provider获取服务使用的时候,没有其他细节 // 使用
    Console.WriteLine(service.Get());

    这段代码表面上看起来好像没有做什么事情,反而饶了一圈,使用Provider获取了一个本身可以直接创建的东西。

    ​ 事实上 services.BuildServiceProvider().GetService() 一般用的比较少,更多的情况是,被ServiceProvider创建的类型是一个入口。而后大部分的业务代码都在这个服务内部。

    ​ 关键的地方就在这里,这个DI支持构造函数注入,也就是说,上方代码里指定获取了ITestService,会根据上方的注册帮我们构造一个TestService对象。而这个TestService对象本身在构造函数里其实是需要IUserService的,但是服务在被获取的时候压根就没有提及IUserService。(将在下文解释)所以当我们需要一个ITestService的时候,其实只需要写代码需要这个服务本身,而不关心任何一个其他的细节。

    由下面代码不难看出,哪怕我们写代码的时候暂时缺失了好几部分的细节实现,也可以先定义一个接口(契约),直接完成应用层的代码逻辑编写。

    public class OrderService
    {
    public IUserService UserService { get; }
    public IPaymentService PaymentService { get; }
    public ILogger<OrderService> Logger { get; } public OrderService(IUserService userService, IPaymentService paymentService, ILogger<OrderService> logger)
    {
    UserService=userService;
    PaymentService=paymentService;
    Logger=logger;
    } /// <summary>
    /// 下单(假的方法)
    /// </summary>
    public void Order(int productId, int quantity) { }
    } public interface IUserService { }
    public interface IPaymentService { }
    1. ​ 这里注入了 用户服务、支付服务、日志服务,然后直接把服务存到自己的属性里,用于Order方法内使用。这一整个过程中,没有涉及到类似于:数据库、支付、日志实现 等细节,直接拿来就用,完全没有关心具体实现。

    2. 这里注入的内容几乎都是接口,而具体注入什么具体实现,不是当前服务决定的,而是交给了上层。

    3. 当使用模块化思想开发的时候,具体实现都分别在不同的项目里都是很常见的情况

    4. 配置的地方事实上在入口的 services.AddTransient<,>()这个方法那里,所以如果出现无法正常构建对象,一般是漏了注册这个动作。

  2. WebApi 程序

    1. 创建一个WebApi项目

      var builder = WebApplication.CreateBuilder(args);
      
      builder.Services.AddControllers();
      
      // 这里注册了一个服务,表示当注入IOrderService的时候,提供用个OrderService
      builder.Services.AddTransient<IOrderService, OrderService>(); // 在app被创建出来之前,进行服务注册
      var app = builder.Build();
      app.MapControllers();
      app.Run();
    2. 比如说在 TestController 控制器里使用

          /// <summary>
      /// 测试
      /// </summary>
      [Route("api/test")]
      [ApiController]
      public class TestController : ControllerBase
      {
      // 仅仅是在构造函数里注入,保存到属性即可
      public IOrderService OrderService { get; }
      public TestController(IOrderService orderService)
      {
      OrderService=orderService;
      } [HttpGet]
      public string Order()
      {
      // 下单了100个id为1的产品
      OrderService.Order(1, 100);
      return "2131231231233123";
      }
      }
  3. 封装批量注入

    1. 定义三个对应三种生命周期的接口,用于控制是否注册到容器

          public interface ITransient { }
      public interface IScoped { }
      public interface ISingleton { }
    2. 增加拓展方法

      using System.Reflection;
      using Microsoft.Extensions.Configuration; namespace Microsoft.Extensions.DependencyInjection
      {
      /// <summary>
      /// 为IServiceCollection拓展批量注册的方法
      /// </summary>
      public static class CApplicationExtensions
      {
      /// <summary>
      /// 注册入口程序集以及关联程序集的所有标记了特定接口的服务到容器
      /// </summary>
      /// <param name="services">容器</param>
      /// <returns>容器本身</returns>
      public static IServiceCollection RegisterAllServices(this IServiceCollection services)
      {
      var entryAssembly = Assembly.GetEntryAssembly(); // 获取所有类型
      var types = entryAssembly!.GetReferencedAssemblies()
      .Select(Assembly.Load)
      .Concat(new List<Assembly>() { entryAssembly })
      .SelectMany(x => x.GetTypes())
      .Distinct();
      // 三种生命周期分别注册(实现得可能不是很好,仅演示,事实上有很多现成的框架可用)
      Register<ITransient>(types, services.AddTransient, services.AddTransient);
      Register<IScoped>(types, services.AddScoped, services.AddScoped);
      Register<ISingleton>(types, services.AddSingleton, services.AddSingleton); return services;
      }
      /// <summary>
      /// 根据服务标记的生命周期 interface,不同生命周期注册到容器里。
      /// </summary>
      /// <param name="types">类型集合</param>
      /// <param name="register">委托:成对注册</param>
      /// <param name="registerDirectly">委托:直接注册服务实现</param>
      /// <typeparam name="TLifetime">注册的生命周期</typeparam>
      private static void Register<TLifetime>(IEnumerable<Type> types, Func<Type, Type, IServiceCollection> register, Func<Type, IServiceCollection> registerDirectly)
      {
      // 找到所有标记了 TLifetime 这个生命周期接口的实现类
      var tImplements = types.Where(t =>
      t.IsClass &&
      !t.IsAbstract &&
      t.GetInterfaces().Any(tinterface => tinterface == typeof(TLifetime)));
      // 遍历,挨个以其他所有接口为key,当前实现为value注册到容器里。
      foreach (var t in tImplements)
      {
      var interfaces = t.GetInterfaces().Where(ti => ti != typeof(TLifetime));
      if (interfaces.Any())
      {
      foreach (var i in interfaces)
      {
      register(i, t);
      }
      }
      // 有时候需要直接注入实现类本身,这里也添加上
      registerDirectly(t);
      }
      }
      }
      }
    3. 入口调用 services.RegisterAllServices(); 注册后,即可通过给服务实现标记 ITransient等接口,让这个拓展方法自动帮我们完成注册的动作。

  4. 最后再提供一个自己通过 Dictionary 练手的一个简单的实现,供参考

    namespace DIDemo
    {
    public static class DictionaryDemo
    {
    /// <summary>
    /// 使用字典实现一个最简单的不带生命周期控制的容器
    /// </summary>
    public static void TypeDictionary()
    {
    // 定义一个字典
    var services = new Dictionary<Type, Type>(); // 注册服务:添加键值对到字典中放着
    services.AddTransient<ITestService, TestService>();
    services.AddTransient<IUserService, UserService>();
    services.AddTransient<ITest001Service, Test001Service>(); // 获取服务:根据Key从字典中获取到想要的类型
    var service = services.GetService<ITestService>();
    // 使用
    Console.WriteLine(service.Get());
    } /// <summary>
    /// 构建对象逻辑代码
    /// </summary>
    /// <param name="services">容器</param>
    /// <param name="interfaceType">接口类型</param>
    /// <returns>object类型的对象</returns>
    public static object GetService(Dictionary<Type, Type> services, Type interfaceType)
    {
    if (services.ContainsKey(interfaceType))
    {
    Type implementType = services[interfaceType];
    // 获取构造函数
    var ctor = implementType
    // 所有的构造函数
    .GetConstructors()
    // 参数最多的拿出来
    .OrderByDescending(t => t.GetParameters().Count()).FirstOrDefault(); if (ctor is not null)
    {
    // 调用的时候发现有参数
    var parameterTypes = ctor.GetParameters().Select(t => t.ParameterType);
    List<object> pList = new List<object>();
    // 每一个参数类型,构造
    foreach (var pType in parameterTypes)
    {
    var p = GetService(services, pType);
    if (p is not null)
    {
    pList.Add(p);
    }
    } return ctor.Invoke(pList.ToArray());
    }
    } return default!;
    } /// <summary>
    /// 包个好用点的拓展方法
    /// </summary>
    public static Dictionary<Type, Type> AddTransient<TInterface, TImplement>(this Dictionary<Type, Type> services)
    {
    services.Add(typeof(TInterface), typeof(TImplement));
    return services;
    } /// <summary>
    /// 包一个好用点的拓展方法
    /// </summary>
    public static TInterface GetService<TInterface>(this Dictionary<Type, Type> services)
    {
    return (TInterface)GetService(services, typeof(ITestService));
    }
    }
    }

最后

依赖注入真的非常实用,哪怕不是NetCore开发,Framework玩家也可以用起来,利用一些现成的东西,让自己更加容易实现一些解耦,减少未来维护成本,仍然是一个不错的选择。

转载注明出处:https://www.cnblogs.com/wosperry/p/dependency_injection.html

【NetCore】依赖注入的一些理解与分享的更多相关文章

  1. 记录对依赖注入的小小理解和autofac的简单封装

    首先,我不是一个开发者,只是业余学习者.其次我的文化水平很低,写这个主要是记录一下当前对于这块的理解,因为对于一个低水平 的业余学习者来说,忘记是很平常的事,因为接触.应用的少,现在理解,可能过段时间 ...

  2. C# 一个初学者对 依赖注入 IOC 的理解( 含 Unity 的使用)

    通过 人打电话 来谈谈自己对IOC的理解 版本1.0 public class Person { public AndroidPhone Phone { get; set; } public void ...

  3. Asp.NetCore依赖注入和管道方式的异常处理及日志记录

    前言     在业务系统,异常处理是所有开发人员必须面对的问题,在一定程度上,异常处理的能力反映出开发者对业务的驾驭水平:本章将着重介绍如何在 WebApi 程序中对异常进行捕获,然后利用 Nlog ...

  4. 控制反转(IOC) 和依赖注入(DI) 的理解

    1.      IOC(控制反转) inverseof control是spring容器的内核,AOP.声明事务等功能在此基础上开花结果. 2.      通过实例理解IOC概念: 实例:<墨攻 ...

  5. 依赖注入和Guice理解

    理解依赖注入,这篇文章写得非常好,结合spring的依赖注入分析的. http://blog.csdn.net/taijianyu/article/details/2338311/ 大体的意思是: 有 ...

  6. DI(依赖注入)简单理解 NO1

    依赖注入:目的削减程序的耦合度,达到高内聚/低耦合 常用形式:Interface Driven Design接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等等.通过Io ...

  7. 依赖注入的方式测试ArrayList和LinkedList的效率(对依赖注入的再次理解)

    9/20 号再进行学习 在C++中,main函数尽可能的简单,只要调用子函数的一句话就实现了功能. java开发中,controller就相同于是main函数,其他类的方法不在本类中时候, 1.可以用 ...

  8. C#反射与特性(六):设计一个仿ASP.NETCore依赖注入Web

    目录 1,编写依赖注入框架 1.1 路由索引 1.2 依赖实例化 1.3 实例化类型.依赖注入.调用方法 2,编写控制器和参数类型 2.1 编写类型 2.2 实现控制器 3,实现低配山寨 ASP.NE ...

  9. NetCore 依赖注入之服务之间的依赖关系

    简单介绍,直接官方文档 https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspn ...

随机推荐

  1. python selenium + web自动化,切换到新的窗口,元素定位不到?

    问题描述: 自动化由首页切换到分页面,打开了一个新的窗口,不过,定位不到这个窗口的元素,通过开发者工具是可以查到这个元素的 原因是: 因为窗口句柄还停留在上一个页面,所以导致无法定位元素.报错 &qu ...

  2. Centos更换阿里云源

    1.备份 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2.下载新的CentOS-Base ...

  3. lvgl移植—Linux fbdev&evdev(基于LVGL v7)

    虽然lvgl官方提供了有关linux framebuffer操作的库函数,但是我决定自己试一下能否自己实现这部分操作 实际项目中应优先采用官方库函数,官方实现代码位于文件夹lv_drivers/dis ...

  4. 灵雀云入选Gartner 2020中国ICT技术成熟度曲线报告,容器技术处于顶峰

    近日,全球权威咨询分析机构Gartner发布了"2020中国ICT技术成熟度曲线(Hype Cycle for ICT in China, 2020 )"报告,灵雀云作为国内容器和 ...

  5. 【Java】final

    final final可以用来修饰的结构:类.方法.变量 final 用来修饰一个类:此类不能被其他类所继承. 比如:String类.System类.StringBuffer类 final 用来修饰方 ...

  6. RHCSA 第六天

    一.  创建下列用户.组和组成员资格: 1.创建名为 sysmgrs 的组 2.创建用户 natasha 同时指定sysmgrs作为natasha的附加组 3.创建用户 harry 同时指定 sysm ...

  7. docker安装、下载镜像、容器的基本操作

    文章目录 一.docker安装与基本使用 1.docker的安装.从远程仓库下载镜像 2.配置docker国内源 二.创建容器 1.create i.创建容器 ii.进入容器 iii.启动容器 2.r ...

  8. RocketMQ 原理:消息存储、高可用、消息重试、消息幂等性

    目录 消息存储 消息存储方式 非持久化 持久化 消息存储介质 消息存储与读写方式 消息存储结构 刷盘机制 同步刷盘 异步刷盘 小结 高可用 高可用实现 主从复制 负载均衡 消息重试 顺序消息重试 无序 ...

  9. manjaro20软件商店无法链接下载

    软件商店如果无法链接下载 解决方案1 可以使用terminal慢慢下载,.bashrc中配置代理 如果依然不行,检查网络设置代理是否为自动或者手动设置正确. 解决方案2 检查是否未设置中国社区源或者重 ...

  10. 解决new Thread().Start导致高并发CPU 100%的问题

    背景 之前接手一个项目的时候,发现到处是 new Thread(()=>{ //do something }).Start(); 这么做的目的,无非是为了减少页面等待时间提高用户体验,把一些浪费 ...