文章目录
概念
控制反转(Inversion of Control ,IOC)和依赖注入(Dependency Injection ,DI)是两个紧密相关的设计概念,常被一同提及但又容易混淆。依赖注入是控制反转实现思想的实现方式。依赖注入的提出是为了简化模块的组装过程,降低模块之间的耦合度。以下从为什么需要控制反转,以及如何实现依赖注入来展开详细解析。
一、控制反转
1.为什么需要控制反转?
传统项目我们常常会大量的重复new一个对象,控制权在自身。这样当依赖变化时,必须修改对象的代码。若修改依赖的实现,所有使用该依赖的类都需修改代码,牵一发而动全身,随着项目的持续推进要修改的代码量会呈现指数型上升。重复创建对象也会存在浪费内存的现象和资源泄漏的风险。并且高层模块直接依赖低层模块,而不是抽象接口,也违背了依赖倒置原则。毕竟相较于相对于低层实现类细节的多变性,抽象的东西要稳定的多
如果要修改对日志(LogOperation)依赖的实现 ,则所有使用该依赖的类都需修改代码,耦合度非常高
public class EnergyConsumeHandler : HandlerBase, IRequiresSessionState
{
public EnergyConsumeHandler () : base(HttpContext.Current.Request.HttpMethod) { }
/*...*/
LogOperation logOperation = new LogOperation();
/*...*/
}
2.控制反转的目的
控制反转的提出本质上是为了提升系统灵活性和扩展性。忽略细节,依赖抽象。体现在实际开发中便是一个控制权的转变, 从 如何创建一个对象
改变成我要一个对象
,无需关心这个对象生成的细节。
3. .NET框架如何实现控制反转
通过依赖注入(DI)和服务定位器(ServiceLocation)。
.net通过依赖注入来实现控制反转,但包含ServiceLocator的功能,下面以.NET8框架简单讨论一下
1.顶级语句 builder.Build()构建对象
var app = builder.Build();
2.WebApplicationBuilder.Build() 核心实现
public WebApplication Build()
{
// ConfigureContainer callbacks run after ConfigureServices callbacks including the one that adds GenericWebHostService by default.
// One nice side effect is this gives a way to configure an IHostedService that starts after the server and stops beforehand.
_hostApplicationBuilder.Services.Add(_genericWebHostServiceDescriptor);
Host.ApplyServiceProviderFactory(_hostApplicationBuilder);
_builtApplication = new WebApplication(_hostApplicationBuilder.Build());
return _builtApplication;
}
3.HostBuilder.Build() 构建服务容器
public HostApplicationBuilder(HostApplicationBuilderSettings? settings){
_createServiceProvider = () =>
{
// Call _configureContainer in case anyone adds callbacks via HostBuilderAdapter.ConfigureContainer<IServiceCollection>() during build.
// Otherwise, this no-ops.
_configureContainer(Services);
return serviceProviderOptions is null ? Services.BuildServiceProvider() : Services.BuildServiceProvider(serviceProviderOptions);
};
}
4.ServiceProvider.GetService 实现服务定位
internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
}
ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);
OnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);
DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);
object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);
System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));
return result;
}
二、依赖注入
1.依赖注入的流程
(1)定义服务(Service)
//天气服务接口
public interface IWeather
{
public List<WeatherForecastDay> WeatherForecast7DayData();
public List<WeatherForecastHour> IWeather.WeatherForecast24HoursData();
}
//-----------------------------------------------------------------------------------------
//天气服务实现类
public class Weather : IWeather
{
public List<WeatherForecastDay> WeatherForecast7DayData()
{
/*.................*/
}
public List<WeatherForecastHour> IWeather.WeatherForecast24HoursData()
{
/*.................*/
}
}
//-----------------------------------------------------------------------------------------
//模型
public class WeatherForecastDay
{
public string? DistrictName { get; set; }
[JsonConverter(typeof(CustomDateTimeConverter), "MM-dd")]
public DateTime Day { get; set; }
public string? WeekDay { get; set; }
public string? WeatherState { get; set; }
public string? Wind { get; set; }
public decimal? Temperature_Max { get; set; }
public decimal? Temperature_Min { get; set; }
}
public class WeatherForecastHour
{
public string? DistrictName { get; set; }
public int Hour { get; set; }
public decimal? Temperature { get; set; }
}
(2)服务容器的构建与注册
·ServiceCollection 是一个用于注册服务的容器类。在 .NET 的依赖注入系统里,它充当了服务注册的起点,可用来收集和管理各类服务的注册信息
//服务容器
ServiceCollection services = new ServiceCollection();
//向服务容器里注册服务
services.AddScoped<IWeather, Weather>();
(3)构造函数注入依赖
通过构造函数注入依赖,高层模块依赖于抽象接口(IWeather)。不需要手动new Weather对象
class DIDemo
{
//构造函数注入
private readonly IWeather _weather;
public DIDemo(IWeather weather)
{
//依赖注入,不需要new Weather
_weather = weather;
}
public void Execute()
{
foreach (WeatherForecastDay weatherForecastDay in _weather?.WeatherForecast7DayData())
{
Console.WriteLine($"{weatherForecastDay.Day}-{weatherForecastDay.DistrictName}" +
$"{weatherForecastDay.Temperature_Max}-{weatherForecastDay.Temperature_Min}");
}
foreach (WeatherForecastHour weatherForecastHour in _weather?.WeatherForecast24HoursData())
{
Console.WriteLine($"{weatherForecastHour.Hour}-{weatherForecastHour.DistrictName}" +
$"{weatherForecastHour.Temperature}");
}
}
}
(4)查询服务
ServiceProvider 是依赖注入系统的核心组件,它负责解析和提供已注册的服务实例。通过调用BuildServiceProvider获取服务对象
private static void UserDI()
{
//构建服务容器
ServiceCollection services = new ServiceCollection();
// 注册服务
services.AddScoped<IWeather, Weather>();
// 构建服务提供程序并创建作用域
using (ServiceProvider provider = services.BuildServiceProvider()) {
using (provider.CreateScope())
{
DIDemo di = provider.GetRequiredService<DIDemo>();
di.Execute();
}
}
}
(5)对象生命周期
(a)瞬态(Transient)
每次从服务容器中请求瞬态生命周期的服务时,都会创建一个新的实例。也就是说,无论在同一个作用域内还是不同作用域内,每次解析该服务都会得到一个全新的对象。
使用 AddTransient 方法进行注册。适用于轻量级、无状态的服务。但频繁创建实例会消耗大量的系统资源,比如内存和 CPU 时间,最终可能导致性能下降,谨慎使用。
//瞬态
services.AddTransient <IWeather, Weather>();
(b)作用域(Scoped)
在同一个作用域内,每次请求作用域生命周期的服务时,都会返回同一个实例;而在不同的作用域内,会创建不同的实例。在 Web 应用中,一个 HTTP 请求通常会创建一个新的作用域
使用 AddScoped 方法进行注册。和数据库交互的服务通常采用 Scoped 生命周期来注册
//作用域
services.AddScoped <IWeather, Weather>();
(c)单例(Singleton)
在整个应用程序的生命周期内,单例生命周期的服务只会创建一个实例。无论在哪个作用域内请求该服务,都会返回同一个实例。
使用 AddSingleton 方法进行注册。适用于需要全局共享状态的服务,如配置信息、缓存服务
//单例
services.AddSingleton <IWeather, Weather>();
2.依赖注入的传染性
依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但如果一个对象是手动new 出来的,那么这个对象就和DI没有关系,他的构造函数中声明的服务类型参数就不会被自动赋值。
如下代码,WeatherController 依赖于 ILogOperation 和 IWeatherDataRequest 接口,而 WeatherDataRequest 依赖于 IRedisTool 接口。依赖关系从 WeatherController 传递到了 WeatherDataRequest,再到 IRedisTool。这种依赖传递就像 “传染病” 一样,使得整个依赖链上的组件都需要通过依赖注入来解决依赖问题。
这样在注册服务时,所有相关的依赖都要在ServiceCollection 的实例进行注册,才能保证创建 WeatherController 实例时,依赖注入容器能够正确地解析并提供所有必要的依赖。
//程序主入口
public class Program
{
static void Main(string[] args)
{
// 创建一个 ServiceCollection 实例,用于注册服务
ServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<WeatherController>();
serviceCollection.AddScoped<ILogOperation, LogOperation>();
serviceCollection.AddScoped<IRedisTool, RedisTool>();
serviceCollection.AddScoped<IWeatherDataRequest, WeatherDataRequest>();
// 构建服务提供程序,用于解析服务实例
using (ServiceProvider provider = serviceCollection.BuildServiceProvider())
{
using (provider.CreateScope())
{
WeatherController weatherController = provider.GetRequiredService<WeatherController>();
weatherController.ExecuteWeatherDataSync();
}
}
}
}
//天气信息
class WeatherController
{
private readonly ILogOperation _logOperation;
private readonly IWeatherDataRequest _weatherDataRequest;
public WeatherController(ILogOperation logOperation, IWeatherDataRequest weatherDataRequest)
{
_logOperation = logOperation;
_weatherDataRequest = weatherDataRequest;
}
//同步天气信息
public void ExecuteWeatherDataSync()
{
_logOperation.RecordLog("------开始执行------");
_weatherDataRequest.GetWeatherData();
_logOperation.RecordLog("------执行结束------");
}
}
//日志记录类
interface ILogOperation
{
void RecordLog(string message);
}
class LogOperation : ILogOperation
{
public void RecordLog(string message)
{
Console.WriteLine($"Log:{message}");
}
}
//redis服务,保存天气信息
interface IRedisTool
{
public string SaveCurrentWeather(string weatherData);
}
class RedisTool : IRedisTool
{
public string SaveCurrentWeather(string weatherData)
{
return "保存天气信息到redis缓存,供系统全局调用";
}
}
//天气信息请求
interface IWeatherDataRequest
{
public void GetWeatherData();
}
class WeatherDataRequest : IWeatherDataRequest
{
private readonly IRedisTool _redisTool;
public WeatherDataRequest(IRedisTool redisTool)
{
_redisTool = redisTool;
}
public void GetWeatherData()
{
string weatherData = "通过接口获取到的天气信息";
_redisTool.SaveCurrentWeather(weatherData);
Console.WriteLine("获取天气信息");
}
}
3.依赖注入的实现
(1)构造函数注入(Constructor Injection)最常见
通过类的构造函数声明依赖,由容器在创建实例时自动注入依赖对象。
//注册
builder.Services.AddTransient<IWeather, Weather>();
//构造函数注入
private readonly ILogger<WeatherController> _logger;
private readonly IWeather _weather;
public class WeatherController : ControllerBase
{
public WeatherController(ILogger<WeatherController> logger, IWeather weather)
{
_logger = logger;
_weather = weather;
}
/// <summary>
/// 天气预报24小时数据
/// </summary>
/// <returns></returns>
[HttpGet]
public string WeatherForecast24Hours()
{
JsonHelper<Object> jsonHelper = new JsonHelper<Object>();
var data = _weather.WeatherForecast24HoursData();
if (data.Count <= 0)
{
jsonHelper.status = -1;
jsonHelper.msg = "未查询到天气预报数据!";
}
jsonHelper.data = data;
return jsonHelper.ToJSON();
}
}
(2)属性注入(Property Injection)
//服务
public interface ICustomLoger
{
void Log(string message);
}
public class CustomLoger : ICustomLoger
{
public void Log(string message) => Console.WriteLine($"CustomLoger: {message}");
}
public class EmailService
{
public ICustomLoger CustomLoger { get; set; }
public void SendEmail(string to) => CustomLoger?.Log($"发送邮件到 {to}");
}
//注册
builder.Services.AddTransient<ICustomLoger, CustomLoger>();
builder.Services.AddTransient<EmailService>();
//注入
using (ServiceProvider provider = serviceCollection.BuildServiceProvider()){
EmailService emailService = provider.GetService<EmailService>();
emailService.CustomLoger = provider.GetService<ICustomLoger>(); // 手动注入
emailService.SendEmail("araby");
}
(3)方法注入(Method Injection)
// 方法注入
public class OrderService
{
private ILogger _logger;
// 无参构造函数
public OrderService() { }
// 方法注入
public void SetLogger(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder()
{
//调用日志服务
_logger.LogWarning("处理购买请求中...");
}
}
// 在 Startup.cs 中注册服务
services.AddTransient<OrderService>();
// 在控制器中使用
[Route("[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly OrderService _orderService;
private readonly ILogger<OrderController> _logger;
public OrderController(OrderService orderService, ILogger<OrderController> logger)
{
_orderService = orderService;
_logger = logger;
}
[HttpGet]
public IActionResult Checkout()
{
// 手动注入依赖
_orderService.SetLogger(_logger);
_orderService.ProcessOrder();
return Ok();
}
}
总结
控制反转(IOC)和依赖注入(DI)是紧密相关的设计概念,DI 是 IOC 的实现方式,提出 IOC 是为解决传统项目中因重复 new 对象导致的依赖变化时代码修改量大、耦合度高、内存浪费、违背依赖倒置原则等问题,其目的是提升系统灵活性和扩展性,转变控制权。