.NET-DI和IoC

.NET-DI和IoC

在 C# 开发中,依赖注入(Dependency Injection, DI)和控制反转(Inversion of Control, IoC)是两个非常重要的设计模式

它们帮助实现松耦合、高内聚的软件架构

DI的相关概念:

  • 服务
  • 注册服务
  • 服务容器
  • 查询服务
  • 对象生命周期
    • Transient:瞬态
    • Scoped:范围
    • Singleton:单例

服务容器

实例化ServiceCollection服务容器,并使用AddXXX方法将对象加入到容器中(可选择不同的生命周期)

using Microsoft.Extensions.DependencyInjection;

namespace 依赖注入
{
  internal class Program
  {
      static void Main(string[] args)
      {
          // 创建服务容器
          var serviceCollection = new ServiceCollection();
          // 注册服务,将对象加入到IoC容器中
          // 对象生命周期设置为瞬态
          serviceCollection.AddTransient<ITestService, TestServiceImpl>();

          // 构建服务提供者
          using (ServiceProvider sp = serviceCollection.BuildServiceProvider())
          {
              // 解析服务
              var testServiceImpl = sp.GetService<ITestService>();
          }
          

      }
  }

  interface ITestService
  {
  }

  class TestServiceImpl : ITestService
  {
  }
}

生命周期的选择

当类中无状态(即无属性和字段)选择Singleton

如果类中有状态,且有scope控制,建议Scope,通常Scope控制下的代码都是运行在同一线程中的,并没有并发修改的问题

使用Transient的时候应该谨慎选择


服务提供者

与Java不同,C#的容器注入方式不是属性注入,而是构造器注入,所以加入到IoC容器中的类无法直接实例化出来

所以如果想在main中调用第一个类,则不能实例化,需要使用服务提供者调用GetXXX方法来获取到此对象

GetService

使用GetService方法获取容器中对象时,如果容器中没有对象则返回null

// 查询服务,通过ServiceProvide的方式获取到对象
using (ServiceProvider sp = serviceCollection.BuildServiceProvider())
{
    // 如果容器中注册此对象则返回null
    var testServiceImpl = sp.GetService<ITestService>();
}
GetRequiredService

使用GetRequiredService方法获取容器中对象时,如果容器中没有对象则抛出异常

// 查询服务,通过ServiceProvide的方式获取到对象
using (ServiceProvider sp = serviceCollection.BuildServiceProvider())
{
    // 区别于GetService, 如果容器中没有注册此对象会直接抛出异常
    var testServiceImpl = sp.GetRequiredService<ITestService>();
}
GetServices

不同于Java,同一接口可注册多个服务,也就是可以将多个接口的实现类对象放到IoC容器中

直接调用则会获取到最后一个放入容器的对象

也可以通过GetServices获取到改接口注册的全部服务

using Microsoft.Extensions.DependencyInjection;

namespace 依赖注入
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 创建服务容器
            var serviceCollection = new ServiceCollection();
            // 注册服务,将对象加入到IoC容器中
            // 对象生命周期设置为瞬态
            serviceCollection.AddTransient<ITestService, TestServiceImpl>();
            serviceCollection.AddTransient<ITestService, TestServiceImp2>();

            // 解析服务,通过ServiceProvide的方式获取到对象
            using (ServiceProvider sp = serviceCollection.BuildServiceProvider())
            {
                var enumerable = sp.GetServices<ITestService>();
                foreach (var testService in enumerable)
                {
                    Console.WriteLine(testService);
                }
            }
        }
    }

    interface ITestService
    {
    }

    class TestServiceImpl : ITestService
    {
    }

    class TestServiceImp2 : ITestService
    {
    }
}

扩展方法

在 C# 中,扩展方法是一种非常有用的特性,它允许你在现有类型上添加新的方法,而无需修改该类型的源代码

这对于增强第三方库中的类型或你无法修改的类型特别有用

基本概念
  • 静态类:扩展方法必须定义在一个静态类中。

  • 静态方法:扩展方法本身必须是静态方法。

  • this 关键字:扩展方法的第一个参数必须使用 this 关键字,后面跟着要扩展的类型。

方法封装

为IServiceCollection接口添加扩展方法,将Add XXX方法进行二次封装

使用扩展方法对注册服务的方法进行封装

using Microsoft.Extensions.DependencyInjection;

namespace LogServices;
/**
 * 此类用于编写有关于ConsoleLog的扩展方法
 */
public static class ConsoleLogExtensions
{
    public static void AddConsoleLog(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddScoped<ILogProvider,ConsoleLogProvider>();
    }
}
serviceCollection.AddConsoleLog(); // 至此,serviceCollection对象可直接调用扩展方法AddConsoleLog,其作用与下面的方法语义相同
serviceCollection.AddScoped<ILogProvider, ConsoleLogProvider>();

配置读取

如果服务器成为了集群,那么为每台服务器来手动定义配置文件是十分费劲的,我们可以将配置文件中心化,单独做一台服务器专门用于其他服务器调取配置文件使用

但这种配置方式会存在“一改全变”的问题,为了解决这种问题,我们使用可覆盖的配置读取器(编程思想中的override)

当同时存在,服务器配置、环境变量配置、本地文件配置或更多方式是,可覆盖的配置读取器会自动的选择最后一个读取的配置运行