Skip to main content

依赖注入 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 如何简单使用。

// 定义一个容器(可以理解为字典)
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<,>() 这个方法那里,所以如果出现无法正常构建对象,一般是漏了注册这个动作。

封装批量注入

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

总结

最后的话

依赖注入真的非常实用,哪怕不是 .Net Core 开发,Framework 玩家也可以用起来,利用一些现成的东西,让自己更加容易实现一些解耦,减少未来维护成本,仍然是一个不错的选择。

相关阅读