【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 如何簡單使用。

控制台程序/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<,>()這個方法那里,所以如果出現無法正常構建對象,一般是漏了注冊這個動作。

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";
            }
        }
    

封裝批量注入

  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等接口,讓這個拓展方法自動幫我們完成注冊的動作。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM