C# 控制反转与依赖注入的实现


概念

控制反转(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 对象导致的依赖变化时代码修改量大、耦合度高、内存浪费、违背依赖倒置原则等问题,其目的是提升系统灵活性和扩展性,转变控制权。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值