【NetCore】依賴注入的一些理解與分享

依賴注入 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玩家也可以用起來,利用一些現成的東西,讓自己更加容易實現一些解耦,減少未來維護成本,仍然是一個不錯的選擇。

轉載註明出處://www.cnblogs.com/wosperry/p/dependency_injection.html