依赖注入 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. 关于ActiveMQ+Zookeeper做集群时,解决启动报错:java.io.IOException: com/google/common/util/concurrent/internal/InternalFutureFailureAccess

    这个问题我也是无意间碰到的,之前一直是使用单机的ActiveMQ,所以也没这个问题,但是做集群时碰到这个问题,问题是这样子出现的: 首先,我准备了三台虚拟机,然后使用 Replicated Level ...

  2. Oracle打怪升级之路二【视图、序列、游标、索引、存储过程、触发器】

    前言 在之前 <Oracle打怪升级之路一>中我们主要介绍了Oracle的基础和Oracle常用查询及函数,这篇文章作为补充,主要介绍Oracle的对象,视图.序列.同义词.索引等,以及P ...

  3. Linux下Tomcat启动、停止、重新启动

    在Linux系统下,重启Tomcat使用命令操作的! 1.首先,进入Tomcat下的bin目录,${CATALINA_HOME}代表tomcat的安装路径 进入Tomcat安装目录: cd ${CAT ...

  4. iframe 去除边框 背景透明等设置 待修改

    <iframe name="file_frame" src="UploadFile.jsp" frameborder=no  border=0  marg ...

  5. 利用python绘制分析路易斯安那州巴吞鲁日市的人口密度格局

    前言 数据来源于王法辉教授的GIS和数量方法,以后有空,我会利用python来实现里面的案例,这里向王法辉教授致敬. 绘制普查人口密度格局 使用属性查询提取区边界 import numpy as np ...

  6. 安装JavaJDK没有jre环境的解决办法 错误: C:\Program Files\Java\jdk-11.0.7\jre

    安装JDK11 发先没有jre解决办法 在安装目录下执行 bin\jlink.exe --module-path jmods --add-modules java.desktop --output j ...

  7. Keil MDK STM32系列(八) STM32F4基于HAL的PWM和定时器输出音频

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  8. 《剑指offer》面试题13. 机器人的运动范围

    问题描述 地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] .一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左.右.上.下移动一格(不能移动到方格外),也不能进入 ...

  9. 《剑指offer》面试题59 - II. 队列的最大值

    问题描述 请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value.push_back 和 pop_front 的均摊时间复杂度都是O(1). 若队列为空,pop_ ...

  10. k8s中kubeconfig的配置及使用

    1.概述 kubeconfig文件保存了k8s集群的集群.用户.命名空间.认证的信息.kubectl命令使用kubeconfig文件来获取集群的信息,然后和API server进行通讯. 注意:用于配 ...