依赖注入
背景
在想解决上一个文档中提到的同一解决方案中不同项目之间的互相引用涉及到的循环依赖的问题的时候,看到有人说使用依赖注入的方式可以解决,于是去了解了一下。
什么是依赖注入
依赖注入(Dependency Injection)是一种设计模式,用于将对象的依赖关系从类中分离出来,通过外部注入的方式提供依赖。在依赖注入中,对象不再自己创建其依赖的对象,而是通过外部容器(如IOC容器)来提供这些依赖。
我的理解:IOC容器会存储一些类,当需要使用某个类的时候,可以直接从IOC容器中获取,而不需要自己创建(感觉和工厂模式有点像)。当向IOC容器拿实例时,容器会通过对应类的构造方法自动创建实例,并返回给调用者。值得注意的是,容器在使用构造方法创建对象的时候,对于构造方法中的参数是有一定要求的:参数的类型必须能够从IOC容器中获取到。即,如果构造方法中的参数类型没有在IOC容器中注册,那么容器是无法创建该对象的。
如果只是到这里,那么依赖注入和工厂模式感觉就没什么区别了。(其实没怎么详细的学过工厂模式,只是通过一些代码大致感觉工厂模式的样子——使用者不直接调用构造方法,通过在工厂类中封装一些方法间接的获取要构造的对象,而在这些工厂类的方法中我们可以添加一些逻辑判断。)
还有一个重要的点:通过接口隔离的方式,能够将接口和实现类分离,从而实现解耦。这样,在IOC容器中注册的类,只需要实现对应的接口即可,而不需要关心具体的实现类是什么,从而实现了接口和实现类的解耦。
var services = new ServiceCollection();
services.AddSingleton<IWellLogService, WellLogService>();
services.AddScoped<ISDMSteer>();
services.AddScoped<WellLogChart>();
使用.net自带的库Microsoft.Extensions.DependencyInjection时,其实已经包含了依赖注入、依赖查找以及依赖拖拽 (个人感觉这后面两个分别对应了依赖注入使用过程中的两个不同阶段)
如上代码所示,在IOC容器中注册了三个类,其中IWellLogService是一个接口,WellLogService是该接口的一个实现类,ISDMSteer和WellLogChart是两个普通的类。在注册的时候,只需要将接口和对应的实现类注册到IOC容器中即可,而不需要关心具体的实现类是什么,从而实现了接口和实现类的解耦(当然,使用的时候即可以注册具体的类,也可以注册接口,但是在注入接口的时候必须指定对应接口的实现类)。
假设我现在要在ISDMSteer这个类中使用IWellLogService这个接口,那么只需要在ISDMSteer的构造方法中添加IWellLogService这个参数即可,IOC容器会自动创建IWellLogService对应的实现类,并注入到ISDMSteer中。
public class ISDMSteer
{
private readonly IWellLogService _wellLogService;
//由于IWellLogService已经在容器中注册过了,所以通过容器来获取ISDMSteer对象时,容器会自动在构造函数中注入已注册过的参数
public ISDMSteer(IWellLogService wellLogService)
{
_wellLogService = wellLogService;
}
public void DoSomething()
{
// 使用 _wellLogService
_wellLogService.ShowMessag(); // 这里调用的是接口中定义的方法,而具体是哪个实现类实现这个的方法,由IOC容器决定(在注册时的时候已经指定了),这里并不需要关心
}
}
从这里就可以直观看到,使用依赖注入 + 接口隔离,能够很大程度上进行解耦,从而使得代码更加灵活,可维护性更高。(但是明显存在的一个不太好的地方就是当需要添加新的东西的时候,需要有两个操作——在接口中定义,然后还要在实现类中实现)
注册模式
在IOC容器中注册类的方式有三种:
-
AddSingleton:将类注册为单例,即在整个应用程序生命周期中只有一个实例。
-
AddScoped:将类注册为作用域,即在一个请求或会话中(一个作用域中)只有一个实例。
- 常见的作用域
- 在一个http请求中,一个请求就是一个作用域
- 一个业务事务开始到结束,一个事务就是一个作用域(数据库连接操作)
- WinForm项目中,没有内置的作用域边界机制,所以若要使用
AddScoped的话,得需要自己定义作用域边界,例如:
//将窗体的打开 / 关闭事件作为作用域的开始 / 结束。
public class MyForm : Form
{
private IServiceScope _scope;
private IMyScopedService _scopedService;
public MyForm(IServiceProvider serviceProvider)
{
// 创建作用域
_scope = serviceProvider.CreateScope();
_scopedService = _scope.ServiceProvider.GetRequiredService<IMyScopedService>();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
// 窗体关闭时释放作用域
_scope.Dispose();
base.OnFormClosed(e);
}
}
AddScoped的方式并不常见。 - AddTransient:将类注册为瞬态,即每次请求或调用时都会创建一个新的实例。就如同使用new关键字创建对象一样。每次都会创建新的实例。
依赖注入的好处
-
解耦:通过依赖注入,可以将对象的依赖关系从类中分离出来,使得类与类之间的耦合度降低,从而提高了代码的可维护性和可扩展性。
-
便于测试:通过依赖注入,可以方便地替换和模拟依赖对象,从而方便进行单元测试。
-
提高代码的可读性和可维护性:通过依赖注入,可以将对象的创建和依赖关系的管理交给外部容器,使得代码更加简洁和清晰。
依赖注入的缺点
-
增加了系统的复杂性:依赖注入引入了外部容器,增加了系统的复杂性,需要学习和理解依赖注入的相关概念和工具。
-
性能开销:依赖注入需要通过外部容器来创建对象,可能会引入一定的性能开销。
-
调试困难:由于依赖注入引入了外部容器,调试时可能需要更多的工具和技巧来定位问题。
依赖注入的实现方式
- 构造函数注入:通过构造函数将依赖对象注入到类中。这种方式可以确保依赖对象在类实例化时就被正确地初始化。如:
public class MyClass
{
private readonly IDependency _dependency;
public MyClass(IDependency dependency)
{
_dependency = dependency;
}
public void DoSomething()
{
_dependency.DoSomething();
}
}
- 属性注入:通过属性将依赖对象注入到类中。这种方式可以灵活地控制依赖对象的注入时机和方式。如:
public class MyClass
{
private IDependency _dependency;
public IDependency Dependency
{
get { return _dependency; }
set { _dependency = value; }
}
public void DoSomething()
{
_dependency.DoSomething();
}
}
- 方法注入:通过方法将依赖对象注入到类中。这种方式可以更灵活地控制依赖对象的注入时机和方式。如:
public class MyClass
{
private IDependency _dependency;
public void SetDependency(IDependency dependency)
{
_dependency = dependency;
}
public void DoSomething()
{
_dependency.DoSomething();
}
}
注意
在容器中进行注册的时候,还是会存在循环依赖的情况,比如A类中需要B类,B类中需要A类,那么在容器中注册的时候,就会报错。
public class UIService:IWellLogService
{
private readonly IUIService _uiService;
public UIService(IUIService uIService)
{
_uiService = uIService;
}
public void ShowMessag()
{
MessageBox.Show("234");
}
}
public class WellLogService:IUIService
{
private readonly IWellLogService _wellLogService;
public WellLogService(IWellLogService wellLogService)
{
_wellLogService = wellLogService;
}
public void ShowMessage()
{
MessageBox.Show("123");
}
}
如果在注册时使用了这两个类,仍然会报错,提示说出现循环依赖的问题。这时就要考虑使用其他方式来解决这个问题
解决方法1:使用Func<T>
修改循环依赖中的任意一个成员:这里就修改wellLogService
public class WellLogService:IUIService
{
private readonly Func<IWellLogService> _wellLogServiceFactory;
public WellLogService(Func<IWellLogService> wellLogServiceFactory)
{
_wellLogServiceFactory = wellLogServiceFactory;
}
public void ShowMessage()
{
MessageBox.Show("123");
}
}
在注册时:
var services = new ServiceCollection();
//重点是通过Func<T>来注册(应该就是用了委托吧)(具体为什么可以这样,我还没去了解过,感觉有点像是js中的懒加载)
services.AddSingleton<IUIService>(provider => new WellLogService(() => provider.GetService<IWellLogService>()));
services.AddSingleton<IWellLogService, UIService>();
services.AddScoped<ISDMSteer>();
services.AddScoped<WellLogChart>();