依赖注入 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. 【工具】Java转换exe

    一.导出jar包 eclipse中对着要转换的项目,右键,导出 搜索jar,选择jar文件,下一步 选择要输出的项目 继续下一步 选择主程序 完成 二.下载及安装exe4j,并转换jar文件为exe文 ...

  2. CSS基础 定位相关属性的使用方法

    1.相对定位:position:relative: 属性名:position 属性值:relative: 特点:1.相对自己的位置移动 2.配合方位名词移动,如:top.left.right,bott ...

  3. 使用delve调试golang

    目录 前置要求 使用方式 使用funcs查找支持的函数 使用break(b)打断点 使用breakpoints查看当前活动的断点. 使用clear清除断点 使用goroutines查看所有协程 使用s ...

  4. Arrays.sort实现原理

    Collections.sort方法底层就是调用的array.sort方法 比较器的方式 TimSort static void sort(Object[] a, int lo, int hi, Ob ...

  5. Word合并多文档

    图片如果损坏,点击链接: https://www.toutiao.com/i6489785099528176142/ 很多时候,我们需要将两个或者多个文档的内容,放到一起,而最直接的办法就是将多个文档 ...

  6. Eureka原理与架构

    一.原理图 Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址 提供者:启动后向Eureka注册自己信息(地址,提供什么服务) 消费者:向Eureka订阅服务,Eureka会将对应服 ...

  7. 在 python 项目中如何记录日志

    一. 概述 写本文的目的是我在写 python 项目的时候需要记录日志,我忘记怎么处理了,每次都需要去网上查一遍,好记性不如烂笔头, 这里把查阅的内容记录下来,方便以后查找. python 项目中记录 ...

  8. docker创建mysql容器时挂载文件路径后无法启动(已解决)

    系统centos7 docker版本: 解决方法: 在docker run中加入 --privileged=true  给容器加上特定权限,如下 docker run --privileged=tru ...

  9. K8s中的volumes-容器数据存放类型及位置

    学习对象:kubectl explain pod.spec.volumes.pod.spec.containers.image.volumeMounts 介绍Volumes 容器内部也有自己的空间,但 ...

  10. golang中int、float、string数据类型之间的转换

    package main import ( "fmt" "strconv" ) func main() { var num1 int = 88 var num2 ...