- A+
作为一个没有系统学习过依赖注入的开发者而言,如果直接在一个使用依赖注入的框架下进行开发,往往对于依赖注入的存在是没有明显的察觉,通过代码追根溯源你都会看不出对象是从哪里创建的。但这并不影响你进行开发的工作,你可以参照现有代码的使用形式,将需要使用的对象加入到构造函数的参数列表上,你就可以使用对象,调用相应的功能了。
ASP.NET Core中利用容器提供对象的方式有两种:依赖注入形式和Service Locator形式(服务定位器)。但是对于依赖注入而言,往往都是由框架主动的为应用程序提供对象,我们仅提供服务注册和注入的构造函数即可,所以依托于框架的自动化机制,在应用层面依赖注入往往是“无感”的。
为了更好的体验容器的服务注册和消费,并屏蔽这种“无感”,本篇的代码示例将采用Service Locator形式,因为该模式获取对象的主动权还在应用程序上,所以可以让服务的注册和消费形式更加直观的展示,有助于示例展示更多细节特征。但是在实际的应用开发时,我们应该避免使用Service Locator,因为它是官方认定的一种反模式。
1.创建演示项目
下面我们从0到1基于一个控制台应用程序来演示容器的服务注册和消费。由于需要使用依赖注入框架,我们则需在创建项目之后引入依赖注入框架依赖的包,其中包含:
- “Microsoft.Extensions.DependencyInjection”
- “Microsoft.Extensions.DependencyInjection.Abstrations”
如果开发的是ASP.NET Core应用程序,依赖注入的包会作为框架基础自动引入。由于示例是控制台应用没有自动引入,我们可以在NuGet中下载安装。
.NET Core提供的很多基础框架或组件在设计时,都会考虑“抽象和实现”的分离,对于依赖注入框架也不例外,其中抽象的接口等类型会定义在后缀名为Abstrations的抽象包中,对于具体的实现类型则定义在DependencyInjection包中。
2.接口和实现
在使用依赖注入容器提供对象的场景中,某个类型依赖的对象通常定义为接口类型,这促使类型的依赖面向的是接口而不是具体的类型,这有助于应用程序的解耦并遵循依赖倒置原则。我个人认为这种面向接口的编程方式,并非特定于依赖注入的范畴,而是面向对象编程的基本原则。
本示例涉及的接口类型和实现类型的代码如下:
1 public interface IFoo{} 2 public interface IBar{} 3 public interface IFoobar<T1,T2>{} 4 5 public class Foo:IFoo{} 6 public class Bar:IBar{} 7 public class Foobar<T1,T2>:IFoobar<T1,T2> 8 { 9 public T1 Para1 {get;} 10 public T2 Para2 {get;} 11 12 public Foobar(T1 para1,T2 para2) 13 { 14 Para1=para1; 15 Para2=para2; 16 } 17 }
上面的代码分别定义3个接口类型:IFoo、IBar、IFoobar<T1,T2>,并且为每个接口定义了对于的实现类型:Foo、Bar、Foobar<T1,T2>,基于实例代码的内容可以表明:容器不光可以提供普通类型的实例,还可以提供泛型类型的实例。
3.服务注册
术语
在依赖注入的介绍中会大量提到“服务”一词,该称谓是依赖注入中的术语,服务通常代表的是:容器为某个类型提供的依赖对象,与Web领域中的服务并不是一个意思。
为了让容器能够提供某个类型依赖的服务,则必须进行相应服务的注册,服务注册是容器提供服务实例的依据,只有进行了服务注册,容器才能知道需要提供哪些类型的服务。
服务在注册时需要指定相应的服务类型和生命周期模式。服务类型通常为一个接口类型,这以便于我们的服务进行抽象化,在后期可以更好的进行扩展。生命周期模式主要通过ServiceLifetime枚举类表示其中包括:Singleton(单例)、Scoped(作用域)、Transient(暂时)。
注册方法
下面结合本示例的服务类型完成相应的服务注册,代码如下:
1 var provider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar, Bar>() 4 .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) 5 .BuildServiceProvider();
上面的代码中首先实例化了一个ServiceCollection对象,该对象主要用于存放服务注册信息。ServiceCollection对象分别使用了AddTransient、AddScoped、AddSingleton扩展方法对IFoo、IBar、IFoobar服务类型进行了服务注册,用于服务注册的扩展方法不仅指定了不同的生命周期,还为服务类型指定了具体的实现类型。在对所有服务注册后,还调用了BuildServiceProvider方法,该方法可以创建并获取依赖注入的容器对象,该容器对象为IServiceProvider类型。
注册形式
依赖注入的服务注册形式并不局限于上面代码示例中的方式,其中很多方式用于服务的注册,如果想深入了解可以查阅官方文档,但主要的注册形式可以归纳为4种类型:
- 指定服务类型和服务实现类型:AddScoped<IBar, Bar>();
- 直接指定具体的服务实现类型:AddTransient(typeof(Foo));
- 根据服务类型直接提供一个实例:AddSingleton<IFoo>(new Foo());
- 指定一个创建服务实例的工厂方法:AddSingleton<IFoo>(_=> { Foo foo = new Foo(); return foo; });
4.服务消费
在完成了服务注册的工作后,接下来我们可以对服务进行消费。首先通过BuildServiceProvider方法返回的容器对象,在调用GetService方法即可获取指定服务的实例。本实例测试获取服务实例的代码如下:
1 var provider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar, Bar>() 4 .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) 5 .BuildServiceProvider(); 6 7 Console.WriteLine(provider.GetService<IFoo>() is Foo); 8 Console.WriteLine(provider.GetService<IBar>() is Bar); 9 var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>(); 10 Console.WriteLine(foobar.Para1 is Foo); 11 Console.WriteLine(foobar.Para2 is Bar);
运行上面的代码后输出的结果均为True,则表示我们成功的获取到服务实例。再次重申下,以上的服务消费方式是Service Locator形式,为了方便演示才采用的,在实际开发中应当使用依赖注入的形式,关于依赖注入形式的使用方式可以查阅:《ASP.NET Core依赖注入之旅:2.理论概念》文章中的“依赖注入初体验”部分。
5.结语
本篇没有过多的深入依赖注入中的每个细节,而是通过Service Locator(服务定位器)的形式结合代码示例,来体验容器进行服务注册和消费的方式,后续会对其中的服务注册和消费的细节进行详细的介绍,还包括非常重要的生命周期模式。
其实在应用层面使用.NET的依赖注入还是比较简单的,因为其中很多复杂的运行机制都是交给了框架,但是这种简单易用的形式并不能成为我们可以浅表学习依赖注入的借口。毫不夸张的说,整个ASP.NET Core就是建立在依赖注入框架之上的,所以我们必须要对依赖注入有一个系统学习的过程。