【CommunityToolkit.Mvvm】开篇. NET 控制反转(IoC)之依赖注入(DI)
参考
Microsoft.Extensions.DependencyInjection入门https://www.cnblogs.com/zcqiand/p/14257661.html
前置阅读
在阅读本文章之前,你可以先阅读:
- 什么是依赖倒置原则
- 什么是控制反转
- 什么是依赖注入
- 什么是服务生命周期
- 什么是DI容器
简介
using Microsoft.Extensions.DependencyInjection;是.NET Core内置依赖注入模块。
using CommunityToolkit.Mvvm.DependencyInjection;是 mvvm框架的内置的依赖注入模块。
.NET Core 依赖注入的 IServiceCollection
什么是依赖注入(DI)和DI容器?
注册服务
服务生命周期
我们注册服务时会发生什么?
实例注册
控制反转(IoC)
控制反转 有两种实现方式:依赖注入和依赖查找。
什么是依赖注入(DI)和DI容器?
Microsoft依赖项注入容器只是一组类,它们组合到一个代码库中,这个库会自动创建并管理程序中需要的对象。在服务容器中注册依赖关系。.NET6提供了一个内置的服务容器 IServiceProvider。将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
public class ClassA { public void DoWork() { var b = new ClassB(); b.DoStuff(); } } public class ClassB { public void DoStuff() { // ... } }
ClassA 直接依赖ClassB,并且在它的 DoWork() 方法中,new了一个ClassB,然后调用了ClassB的 DoStuff()方法
首先,加一个构造函数,并且指定了ClassA依赖的类型,调用构造函数时,必须提供ClassB的实例, 在ClassA的内部,我们不会去new一个ClassB,ClassB完全是由外部传入的,这里就是控制反转(IOC)
public class ClassA { private readonly ClassB _dependency; public ClassA(ClassB classB) => _dependency = classB; public void DoWork() => _dependency.DoStuff(); } public class ClassB : IThing { public void DoStuff() { // ... } }
加一个接口IThing,现在,我们已经应用了SOLID的依赖倒置原则,我们不再依赖具体的实现,相反,我们依赖于IThing抽象,在构造函数中,只需要传入IThing的实现就好了
public interface IThing { public void DoStuff(); } public class ClassA { private readonly IThing _dependency; public ClassA(IThing thing) => _dependency = thing; public void DoWork() => _dependency.DoStuff(); } public class ClassB : IThing { public void DoStuff() { // ... } }
手动new了一个ClassB,它实现了IThing接口,然后创建ClassA的时候,直接把thing传入构造函数中
class Program { static void Main(string[] args) { IThing thing = new ClassB(); ClassA classA = new ClassA(thing); classA.DoWork(); } }
- 在实际中呢,我们代码中有很多类型,然后有各种各样的依赖关系
- 这个时候我们就需要一个DI容器(.NET 框架自带的一个容器IServiceProvider),我们对容器进行配置,然它知道什么类型,然后负责自动创建并管理对象 (通常称为服务)
依赖注入的实现方式
依赖注入其实并不神奇,我们日常的代码中很多都用到了依赖注入,但很少注意到它,也很少主动使用依赖注入进行解耦。这里我们简单介绍一下赖注入实现三种的方式。
构造函数注入(Contructor Injection)
这是我认为的最简单的依赖注入方式,我们修改一下上面代码中MovieList的构造函数,使得MovieFinderImpl的实现在MovieLister类之外创建。这样,MovieLister就只依赖于我们定义的MovieFinder接口,而不依赖于MovieFinder的实现了。
目前,.NET 6 的依赖注入系统原生只支持构造函数注入,不过在某些类型的 Web 应用扩展下,也支持特定的方法注入、特性注入、甚至属性注入。
public class MovieLister { private MovieFinder finder; public MovieLister(MovieFinder finder) { this.finder = finder; } ... }
setter注入
类似的,我们可以增加一个setter函数来传入创建好的MovieFinder对象,这样同样可以避免在MovieFinder中hard init这个对象。
public class MovieLister { s... public void Finder { get{ this.finder = value; } } }
接口注入
具有入侵性,已经不使用了。
public interface InjectFinder { void injectFinder(MovieFinder finder); } class MovieLister :InjectFinder { ... public void injectFinder(MovieFinder finder) { this.finder = finder; } ... }
依赖注入降低了依赖和被依赖类型间的耦合,在修改被依赖的类型实现时,不需要修改依赖类型的实现,同时,对于依赖类型的测试,可以更方便的使用mocking object替代原有的被依赖类型,以达到对依赖对象独立进行单元测试的目的。
最后需要注意的是,依赖注入只是控制反转的一种实现方式。控制反转还有一种常见的实现方式称为依赖查找。
依赖查找的实现方式
依赖查找有两种实现方式,依赖拖曳和上下文查找。在介绍这两种实现方式之前,要记住依赖查找是当A依赖到B时,A主动向容器中查找B对象(getBean()方法)。依赖拖曳和上下文查找都是这样获取依赖的。两者的不同之处在于如何选择和获取到容器。
依赖拖曳(Dependency Pull)
依赖拖曳获取容器的方法是在代码中写死的。其逻辑是从配置文件(.properties,xml文件等)中生成容器,然后再从容器中查找需要的依赖对象。也就是说,依赖对象必须在指定的配置文件中配置好。
我们来改造文章开头的例子,从代码中看看依赖拖曳如何获取依赖的
public class ServiceImpl : Service
{ public void insertData(String data) { //注意getFactory方法,依赖拖曳和上下文查找只有getFactory的实现不同而已 BeanFactory beanFactory = getBeanFactory(); //得到容器后直接getBean方法查找依赖对象 Dao dao = (Dao) beanFactory.getBean("dao"); dao.insert(data); } private BeanFactory getBeanFactory() { //从指定配置文件中获取容器 XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beans.xml")); return beanFactory; } }
上下文查找
顾名思义,上下文查找的查找方式是和当前环境有关的。与依赖拖曳那样永远通过指定容器的方式不同,上下文查找获取容器的方式是动态的,是由外界传来的。
我们还是以代码为例。
public class ServiceImpl : Service
{ //在方法中传入beanFactory,这就不是从指定配置文件中获取beanFactory了。 public void insertData(String data, BeanFactroy beanFactory) { Dao dao = (Dao) beanFactory.getBean("dao"); dao.insert(data); } }
从代码中可以看出,beanFactory是由外界传来的。那这个beanFactory的来源就不是固定的了,这一点和依赖拖曳不同。
依赖注入(DI)和依赖查找(DL)的比较
关于依赖注入的介绍可以看这篇文章
两者都是符合IoC的一个原则,(A的逻辑中需要B的服务,A依赖B。A是依赖对象,B是被依赖对象)即被依赖对象不再由依赖对象主动创建,创建管理被依赖对象的工作交给容器,依赖对象只管用就行了。
DI和DL相比较,很明显DI的处理方式更符合IoC的思想。毕竟IoC理念提出来就是为了解决耦合问题,让开发人员更专注于业务代码而不用考虑与业务无关的依赖等问题。
从依赖查找的的实现方式中我们可以看到,依赖查找对业务代码是有侵入性的。业务代码中必须包含着获取beanFactory的容器的逻辑,而获取beanFactory明显不属于业务逻辑。
依赖查找唯一比依赖注入好的地方,就是代码的逻辑比较明显。我们能从代码中很明显地看到被依赖对象是从容器中来的,而如何获取到容器也是显而易见的。但依赖查找就没那么明显,被依赖对象的注入方式写得比较隐晦。
注册服务
通常, Microsoft DI 容器需要在Startup类中配置,在这里,您可以使用ConfigureServices方法向容器注册服务,在应用程序托管生命周期的早期,将调用ConfigureServices方法,它有一个参数`IServiceCollection``,这个参数在初始化应用程序时传入
public class Startup { public void ConfigureServices(IServiceCollection services) { // 注册服务 } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { } }
-
为了尽可能的简单,我们也可以在控制台中使用
Microsoft DependencyInjection
-
创建控制台程序后,我们首先在项目中引入
Microsoft.Extensions.DependencyInjection
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" /> </ItemGroup> </Project>
现在我们开始注册我们的服务,但是我们需要一个IServiceCollection
,让我们看一下IServiceCollection的定义
public interface IServiceCollection : IList<ServiceDescriptor> { }
IServiceCollection
没有定义其任何成员,而是从IList<ServiceDescriptor>
派生- 在
Microsoft.Extensions.DepenencyInjection
程序包里面,它有一个默认的实现:ServiceCollection
public class ServiceCollection : IServiceCollection { private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>(); public int Count => _descriptors.Count; public bool IsReadOnly => false; public ServiceDescriptor this[int index] { get { return _descriptors[index]; } set { _descriptors[index] = value; } } // ... }
-
它有一个私有的List集合:
_descriptors
,里面是ServiceDescriptor -
让我们从创建一个ServiceCollection,然后注册两个服务
static void Main(string[] args) { var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton<ClassA>(); serviceCollection.AddSingleton<IThing, ClassB>(); Console.WriteLine("Done"); }
在前面的代码中,我们已经使用AddSingleton方法注册了两个服务,这不是IServiceCollection接口定义的方法,也不在ServiceCollection上,这是IServiceCollection的扩展方法,这个方法在ServiceCollectionServiceExtensions的扩展类中,接下来,我会介绍这个方法是如何注册服务的,不过这之前,我们首先回顾下服务生命周期的概念
服务生命周期
在Microsoft依赖项注入框架中,我们可以使用三种生命周期注册服务,分别是单例(Singleton)、瞬时(Transient)、作用域(Scoped),在上面的代码中,使用了AddSingleton()来注册服务。
使用Singleton服务的优点是我们不会创建多个服务实例,只会创建一个实例,保存到DI容器中,直到程序退出,这不仅效率高,而且性能高,但是有一个要注意的点,如果在多线程中使用了Singleton,要考虑线程安全的问题,保证它不会有冲突, 相当于一共全局的 变量。AddSingleton() 的生命周期为 项目启动 -> 项目结束(或者手动释放 释放后也不会在创建),即存在于整个应用的生命周期内,相当于全局单例模式。
瞬时(Transient)和单例(Singleton)模式是相反的,每次使用时,DI容器都是创建一个新的实例。相当于new 一共局部变量。
作用域(Scoped),在一个作用域内,会使用同一个实例,像EF Core的DbContext上下文就被注册为作用域服务。
我们注册服务时会发生什么?
- 在上面的代码中,已经注册了两个单例服务
serviceCollection.AddSingleton<ClassA>();
serviceCollection.AddSingleton<IThing, ClassB>();
这是最终的AddSingleton方法:
public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Type implementationType) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (serviceType == null) { throw new ArgumentNullException(nameof(serviceType)); } if (implementationType == null) { throw new ArgumentNullException(nameof(implementationType)); } return Add(services, serviceType, implementationType, ServiceLifetime.Singleton); }
-
我们可以看到
AddSingleton
方法调用了私有的Add()
方法,并且传入了一个生命周期的枚举值ServiceLifetime.Singleton
-
让我们看一下Add方法的工作原理:
private static IServiceCollection Add(IServiceCollection collection, Type serviceType, Type implementationType, ServiceLifetime lifetime) { var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); collection.Add(descriptor); return collection; }
它创建一个新的ServiceDescriptor实例,传入服务类型,实现类型(可能与服务类型相同)和生命周期,然后调用Add方法添加到列表中
之前,我们了解到IServiceCollection本质上是包装了List <ServiceDescriptor>, ServiceDescriptor类很简单,代表一个注册的服务,包括其服务类型,实现类型和生命周期
实例注册
我们也可以手动new一个实例,然后传入到AddSingleton()
方法中:
var myInstance = new ClassB(); serviceCollection.AddSingleton<IThing>(myInstance);
- 使用 ServiceDescriptor
- 我们还可以手动定义一个ServiceDescriptor,然后直接添加到
IServiceCollection中
var descriptor = new ServiceDescriptor(typeof(IThing), typeof(ClassB), ServiceLifetime.Singleton); serviceCollection.Add(descriptor);
.NET中的DI的一些核心知识,可以直接创建ServiceCollection来使用Microsoft DI框架,了解了IServiceCollection上的AddSingleton扩展方法是如何工作,以及它们最终创建了一个ServiceDescriptor,然后添加到一个ServiceCollection包装的List集合中
————————————————
版权声明:本文为CSDN博主「黑夜中的潜行者」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43562262/article/details/114438889