控制反转(IoC)和依赖注入(DI)
控制反转(Inversion of Control,IoC)是一种设计原则,它把创建和管理类对象的控制权从调用者转移到框架容器中。事先在框架容器中注册各种类对象,由框架容器统一负责类对象的创建、装配和管理;调用者无须显式创建类对象,只需声明使用即可。在这里,类对象就是服务。IoC进一步实现了各模块之间的解耦,充分体现了技术人在“高内聚、低耦合”设计方向上的不懈追求!
依赖注入(Dependency Injection,DI)是实现IoC的最流行方式,被各大技术框架所采用。
本文详细介绍.NET框架的DI用法,并给出一个Spring框架的DI示例用以对比。
.NET中的依赖注入
使用流程
-
安装NuGet包:
Microsoft.Extensions.DependencyInjection
-
定义服务,一般包括接口和实现接口的具体类,具体类可能会依赖其它的服务(有其它服务类型的成员变量,即关联关系)
- 日志服务:定义日志服务接口并实现一个控制台日志服务
1
2
3
4
5
6
7
8
9
10
11
12public interface ILogService {
public void LogError(string message);
public void LogInfo(string message);
}
public class ConsoleLogService : ILogService {
public void LogError(string message) {
Console.WriteLine($"Error: {message}");
}
public void LogInfo(string message) {
Console.WriteLine($"Info: {message}");
}
}- 配置服务:定义配置服务接口并实现一个环境变量配置服务
1
2
3
4
5
6
7
8public interface IConfigService {
public string? GetValue(string key);
}
public class EnvVarConfigService : IConfigService {
public string? GetValue(string key) {
return Environment.GetEnvironmentVariable(key);
}
}- 邮件服务:定义邮件服务接口并实现具体的邮件服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public interface IMailService {
public void Send(string title, string to, string body);
}
public class MailService : IMailService {
private readonly ILogService _logProvider;
private readonly IConfigReader _configService;
public MailService(ILogService logProvider, IConfigReader configService) {
_logProvider = logProvider;
_configService = configService;
}
public void Send(string title, string to, string body) {
// 先读配置
_logProvider.LogInfo("开始读取配置信息:");
var port = _configService.GetValue("port");
if (port == null)
_logProvider.LogError("读取端口信息失败!");
else
_logProvider.LogInfo($"Port: {port}");
_logProvider.LogInfo("读取配置信息完成!");
// 发送邮件
_logProvider.LogInfo("开始发送邮件:");
Console.WriteLine($"发邮件了:{title},{to},{body}");
_logProvider.LogInfo("发送邮件完成!");
}
} -
使用邮件服务:先注册服务,再定位并使用服务,在实际项目中,服务的注册和使用可以分离
1
2
3
4
5
6
7
8
9
10
11
12
13static void Main(string[] args) {
var services = new ServiceCollection();
// 注册日志、配置、邮件三种服务
services.AddScoped<ILogService, ConsoleLogService>();
services.AddScoped<IConfigService, EnvVarConfigService>();
services.AddScoped<IMailService, MailService>();
// 定位邮件服务发送邮件
using (var sp = services.BuildServiceProvider()) {
var mailService = sp.GetRequiredService<IMailService>();
mailService.Send("第一封邮件", "LiRuohan", "你好,依赖注入!");
}
Console.ReadKey();
}
如果采用传统方式实现邮件服务,我们需要手动创建MailService
的对象,并且在创建之前需要创建ConsoleLogService
和EnvVarConfigService
的对象:
1 | static void Main(string[] args) { |
采用这种方式,一旦需要使用邮件服务,就必须手动创建日志服务对象和配置服务对象,自然而然需要知道三种服务的具体类。如果后期需要用新的服务类替换既有服务类,那么就必须修改所有手动创建服务的代码,这在大型项目中非常令人厌恶!
采用DI方式的代码中,只需要修改注册服务时的服务具体类即可,更加充分体现了“面向接口编程”的思想。
服务注册
创建一个ServiceCollection
对象,就可以向该对象中注册服务了,注册服务完成之后,调用该对象的BuildServiceProvider()
就可以获得ServiceProvider
对象,后续这个对象进行服务定位。
三种生命周期的服务:
AddSingleton()
:- 单例模式,容器只会创建一个示例,每次请求服务时,容器都会返回该实例。
- 适用于无状态服务,例如上面的日志服务和配置服务。
AddScoped()
:- 作用域模式,在同一个作用域内容器会返回相同的实例,在不同的作用域内会重新创建新的实例。
- 作用域理解:一般而言,每次请求服务器时都会创建一个新的作用域,对于非Web应用而言,默认处于一个相同的作用域中,该作用域不同于语法作用域。
- 适用于有状态服务。
AddTransient()
:- 瞬态模式,每次请求服务时,容器都会创建一个新的实例。
- 慎用,对性能可能造成严重影响,需要评估创建新实例的代价。
常见的服务注册语法:
- 直接注册实现类,也可以将实现类和接口一起注册
1 | // 由于在邮件服务中包含两个成员变量:ILogService和IConfigService,针对这两个服务必须同时注册接口和实现类 |
- 类型参数可以用泛型的方法表达,也可以用类型参数的方式表达,一般用前者即可
1 | // 方式1 |
- 如果在构造的时候需要给属性赋值,那么可以用Lambda表达式传参
1 | // 以下是Json文件配置服务 |
- 如果要为同一服务注册多个实现类,可以为每个实现类指定一个key,后期用这进行服务定位
1 | services.AddKeyedScoped<IConfigService>("json", (p, k) => new JsonConfigService() { JsonPath = "./config.json" }); |
- 如果针对同一接口进行了多次注册,服务定位时以最后那个为准
服务定位
通过容器创建的对象,会自动进行依赖注入,手动创建的对象则不会,这就要求显式从容器中获取根对象。
IServiceCollection
的BuildServiceProvider()
方法可以返回ServiceProvider
对象,通过该对象就可以进行服务定位。
GetService<T>()
:获取服务,没有返回空GetRequiredService<T>()
:获取服务,没有抛出异常GetServices<>()
:获取同一接口的所有服务,返回服务数组- 也可以通过给定的key名称定位服务
拓展
- 服务提供者提供服务注册快捷函数,避免服务注册者显式声明服务类,这在.NET框架中被大量使用,主要思路是编写
IServiceCollection
的扩展方法
1 | namespace Microsoft.Extensions.DependencyInjection { |
1 | // 使用时这样即可 |
- 配置覆盖机制,在实际工程中,项目配置可能会集中放置在一台服务器,但是开发过程中应该允许对默认配置进行覆盖,这时可以采用下列方式,在
LayeredConfigReader
中_services
会捕捉注册的所有IConfigService
服务,在GetValue
中就可以按照顺序遍历服务获取值
1 | public interface IConfigReader { |
Spring中的依赖注入
当前,Web开发的框架百花齐放,但是它们的很多原理是相似的,在Spring框架中也有DI机制,且DI是Spring框架的实现基石,下面将上述.NET实例改成Spring实例。
- 引入相应的包,使之支持DI
1 | <dependencies> |
- 日志服务
1 | package com.example.testdi; |
1 | package com.example.testdi; |
- 配置服务
1 | package com.example.testdi; |
1 | package com.example.testdi; |
- 邮件服务
1 | package com.example.testdi; |
1 | package com.example.testdi; |
- 主程序
1 | package com.example.testdi; |
Spring中的依赖注入有两种形式:①基于XML配置文件的依赖注入;②基于注解的依赖注入。后者更为简洁灵活,是最常用的方式。
Spring依赖注入的服务注册和服务定位与.NET有很大差异,后者通过代码集中注册;前者采用“注解+扫描”的分布式方式,更为灵活。
Autofac依赖注入库
使用流程
为方便对比,将上述.NET实例的DI库调整为Autofac,服务定义与原实例相同,变化体现在服务注册和定位上:
1 | static void Main(string[] args) { |
服务注册
服务的生命周期:
- 单例模式:
builder.RegisterType<ConsoleLogService>().As<ILogService>().SingleInstance()
- 作用域模式:
builder.RegisterType<ConsoleLogService>().As<ILogService>().InstancePerLifetimeScope()
- 瞬态模式:
builder.RegisterType<ConsoleLogService>().As<ILogService>().InstancePerDependency()
,不指定什么周期默认就是瞬态模式
服务的注册语法:
- 直接注册实现类,也可以将实现类和接口一起注册
1 | // 原生库的服务注册 |
- 类型参数可以用泛型的方法表达,也可以用类型参数的方式表达,一般用前者即可
1 | // 方式1 |
- 如果在构造的时候需要给属性赋值,那么可以用Lambda表达式传参
1 | builder.Register(c => new JsonConfigService() { JsonPath = "config.json" }).As<IConfigService>().InstancePerLifetimeScope(); |
- 如果要为同一服务注册多个实现类,可以为每个实现类指定一个key,后期用这进行服务定位
1 | builder.Register(c => new JsonConfigService() { JsonPath = "config.json" }).As<IConfigService>().Named<IConfigService>("json").InstancePerLifetimeScope(); |
- 如果针对同一接口进行了多次注册,服务定位时以最后那个为准
服务定位
ContainerBuilder
的Build()
方法可以返回IContainer
对象,通过该对象就可以进行服务定位。
Resolve<T>()
:获取服务,没获取到服务是抛出异常TryResolve<T>()
:获取服务,没有获取到时返回false
- 也可以通过给定的key名称定位服务:
container.ResolveKeyed<IConfigService>("json")
属性注入
.NET的原生DI库无法实现属性注入,Autofac可以实现,即服务的一个属性也可以通过依赖注入自动赋值。
1 | public interface IUserRepository { |
本实例中,
UserRepository
通过构造函数注入成员变量_connection
,通过属性注入Logger
。如果一个类中有属性需要进行注入,在注册该类时需要通过方法
PropertiesAutowired()
开启属性注入。
(转载本站文章请注明作者和出处lihaohello.top,请勿用于任何商业用途)