依赖注入 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. rsync配置文件讲解

    1.安装rysnc 一般在安装系统时rsync是安装上(yum安装) 2.     vim /etc/xinetd.d/rsync 在这个路径下有配置文件 service rsync { disabl ...

  2. Python基础案例练习:制作学生信息管理系统

    一.前言 学生信息管理系统,相信大家或多或少都有做过 最近看很多学生作业都是制作一个学生信息管理系统 于是,今天带大家做一个简单的学生信息管理系统 二.开发环境: 我用到的开发环境 Python 3. ...

  3. vue 从后台获取数据并渲染到页面

    一.在 created中调用methods中的方法 二.在methods中通过vuex异步获取后台数据 三.在computed 中计算属性 四.页面中调用computed中的计算后的属性 来自为知笔记 ...

  4. Tomcat8/9的catalina.out中文乱码问题解决

    OS: Red Hat Enterprise Linux Server release 7.8 (Maipo) Tomcat: 9 中文显示为???问号 在$CATALINA_HOME/conf下的l ...

  5. idea同时启动多个微服务模块进行管理

    1,打开IDEA项目中的 .idea 下 的workspace.xml 找到文件中的 RunDashboard 配置块,增加如下圈起来的地方 代码: <option name="con ...

  6. Maven Archetype 多 Module 自定义代码脚手架

    大部分公司都会有一个通用的模板项目,帮助你快速创建一个项目.通常,这个项目需要集成一些公司内部的中间件.单元测试.标准的代码格式.通用的代码分层等等. 今天,就利用 Maven 的 Archetype ...

  7. unittest测试框架

    unittest单元测试框架不仅可以适用于单元测试,还可以适用WEB自动化测试用例的开发与执行,该测试框架可组织执行测试用例,并且提供了丰富的断言方法,判断测试用例是否通过,最终生成测试结果. 一.u ...

  8. 封装OCX

    封装OCX的办法有2种: 1. 使用C++的MFC activex项目生成OCX 2. 使用C#的用户控件生成OCX(.net core好像不支持) 注意:以管理员身份运行Visual Studio ...

  9. C# 文件对话框例子

    OpenFileDialog控件的基本属性InitialDirectory:对话框的初始目录 Filter: 获取或设置当前文件名筛选器字符串,例如,"文本文件(*.txt)|*.txt|所 ...

  10. gorm中的更新

    保存所有字段 Save 会保存所有的字段,即使字段是零值. db.First(&user, 5)user.Name = sql.NullString{"王八", true} ...