ASP.NET Core中的依赖注入(2):依赖注入(DI)
IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用“好莱坞原则”是应用程序以被动的方式实现对流程的定制。我们可以采用若干设计模式以不同的方式实现IoC,比如我们在上面介绍的模板方法、工厂方法和抽象工厂,接下来我们介绍一种更为有价值的IoC模式,即依赖注入(DI:Dependency Injection,以下简称DI)。
目录
一、由外部容器提供服务对象
二、三种依赖注入方式
构造器注入
属性注入
方法注入
三、实例演示:创建一个简易版的DI框架
一、由外部容器提供服务对象
和上面介绍的工厂方法和抽象工厂模式一样,DI旨在实现针对服务对象的动态提供。具体来说,服务的消费者利用一个独立的容器(Container)来获取所需的服务对象,容器自身在提供服务对象的过程中会自动完成依赖的解析与注入。话句话说,由DI容器提供的这个服务对象是一个” 开箱即用”的对象,这个对象自身直接或者间接依赖的对象已经在初始化的工程中被自动注入其中了。
举个简单的例子,我们创建一个名为Cat的DI容器类,那么我们可以通过调用具有如下定义的扩展方法GetService<T>从某个Cat对象获取指定类型的服务对象。我之所以将其命名为Cat,源于我们大家都非常熟悉的一个卡通形象“机器猫(哆啦A梦)”。它的那个四次元口袋就是一个理想的DI容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。DI容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务对象。
1: public static class CatExtensions
2: {
3: public static T GetService<T>(this Cat cat);
4: }
对于我们在上一篇演示的MVC框架,我们在前面分别采用不同的设计模式对框架的核心类型MvcEngine进行了改造,现在我们采用DI的方式并利用上述的这个Cat容器按照如下的方式对其进行重新实现,我们会发现MvcEngine变得异常简洁而清晰。
1: public class MvcEngine
2: {
3: public Cat Cat { get; private set; }
4:
5: public MvcEngine(Cat cat)
6: {
7: this.Cat = cat;
8: }
9:
10: public void Start(Uri address)
11: {
12: while (true)
13: {
14: Request request = this.Cat.GetService<Listener>().Listen(address);
15: Task.Run(() =>
16: {
17: Controller controller = this.Cat.GetService<ControllerActivator>().ActivateController(request);
18: View view = this.Cat.GetService<ControllerExecutor>().ExecuteController(controller);
19: this.Cat.GetService<ViewRenderer>().RenderView(view);
20: });
21: }
22: }
23: }
DI体现了一种最为直接的服务消费方式,消费者只需要告诉生产者(DI容器)关于所需服务的抽象描述,后者根据预先注册的规则提供一个匹配的服务对象。这里所谓的服务描述主要体现为服务接口或者抽象服务类的类型,当然也可以是包含实现代码的具体类型。至于应用程序对由框架控制的流程的定制,则可以通过对DI容器的定制来完成。如果具体的应用程序需要采用上面定义的SingletonControllerActivator以单例的模式来激活目标Controller,那么它可以在启动MvcEngine之前按照如下的形式将SingletonControllerActivator注册到后者使用的DI容器上。
1: public class App
2: {
3: static void Main(string[] args)
4: {
5: Cat cat = new Cat().Register<ControllerActivator, SingletonControllerActivator>();
6: MvcEngine engine = new MvcEngine(cat);
7: Uri address = new Uri("http://localhost/mvcapp");
8: Engine.Start(address);
9: }
10: }
二、三种依赖注入方式
一项确定的任务往往需要多个对象相互协作共同完成,或者某个对象在完成某项任务的时候需要直接或者间接地依赖其他的对象来完成某些必要的步骤,所以运行时对象之间的依赖关系是由目标任务来决定的,是“恒定不变的”,自然也无所谓“解耦”的说法。但是运行时的对象通过设计时的类来定义,类与类之间耦合则可以通过依赖进行抽象的方式来解除。
从服务使用的角度来讲,我们借助于一个服务接口对消费的服务进行抽象,那么服务消费程序针对具体服务类型的依赖可以转移到对服务接口的依赖上。但是在运行时提供给消费者总是一个针对某个具体服务类型的对象。不仅如此,要完成定义在服务接口的操作,这个对象可能需要其他相关对象的参与,换句话说提供的这个服务对象可能具有针对其他对象的依赖。作为服务对象提供者的DI容器,在它向消费者提供服务对象之前会自动将这些依赖的对象注入到该对象之中,这就是DI命名的由来。
如右图所示,服务消费程序调用GetService<IFoo>()方法向DI容器索取一个实现了IFoo接口的某个类型的对象,DI容器会根据预先注册的类型匹配关系创建一个类型为Foo的对象。此外,Foo对象依赖Bar和Baz对象的参与才能实现定义在服务接口IFoo之中的操作,所以Foo具有了针对Bar和Baz的直接依赖。至于Baz,它又依赖Qux,那么后者成为了Foo的间接依赖。对于DI容器最终提供的Foo对象,它所直接或者间接依赖的对象Bar、Baz和Qux都会预先被初始化并自动注入到该对象之中。
从编程的角度来讲,类型中的字段或者属性是依赖的一种主要体现形式,如果类型A中具有一个B类型的字段或者属性,那么A就对B产生了依赖。所谓依赖注入,我们可以简单地理解为一种针对依赖字段或者属性的自动化初始化方式。具体来说,我们可以通过三种主要的方式达到这个目的,这就是接下来着重介绍的三种依赖注入方式。
构造器注入
构造器注入就在在构造函数中借助参数将依赖的对象注入到创建的对象之中。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性Bar上,针对该属性的初始化实现在构造函数中,具体的属性值由构造函数的传入的参数提供。当DI容器通过调用构造函数创建一个Foo对象之前,需要根据当前注册的类型匹配关系以及其他相关的注入信息创建并初始化参数对象。
1: public class Foo
2: {
3: public IBar Bar{get; private set;}
4: public Foo(IBar bar)
5: {
6: this.Bar = bar;
7: }
8: }
除此之外,构造器注入还体现在对构造函数的选择上面。如下面的代码片段所示,Foo类上面定义了两个构造函数,DI容器在创建Foo对象之前首选需要选择一个适合的构造函数。至于目标构造函数如何选择,不同的DI容器可能有不同的策略,比如可以选择参数做多或者最少的,或者可以按照如下所示的方式在目标构造函数上标注一个相关的特性(我们在第一个构造函数上标注了一个InjectionAttribute特性)。
1: public class Foo
2: {
3: public IBar Bar{get; private set;}
4: public IBaz Baz {get; private set;}
5:
6: [Injection]
7: public Foo(IBar bar)
8: {
9: this.Bar = bar;
10: }
11:
12: public Foo(IBar bar, IBaz):this(bar)
13: {
14: this.Baz = baz;
15: }
16: }
属性注入
如果依赖直接体现为类的某个属性,并且该属性不是只读的,我们可以让DI容器在对象创建之后自动对其进行赋值进而达到依赖自动注入的目的。一般来说,我们在定义这种类型的时候,需要显式将这样的属性标识为需要自动注入的依赖属性,以区别于该类型的其他普通的属性。如下面的代码片段所示,Foo类中定义了两个可读写的公共属性Bar和Baz,我们通过标注InjectionAttribute特性的方式将属性Baz设置为自动注入的依赖属性。对于由DI容器提供的Foo对象,它的Baz属性将会自动被初始化。
1: public class Foo
2: {
3: public IBar Bar{get; set;}
4:
5: [Injection]
6: public IBaz Baz {get; set;}
7: }
方法注入
体现依赖关系的字段或者属性可以通过方法的形式初始化。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性上,针对该属性的初始化实现在Initialize方法中,具体的属性值由构造函数的传入的参数提供。我们同样通过标注特性(InjectionAttribute)的方式将该方法标识为注入方法。DI容器在调用构造函数创建一个Foo对象之后,它会自动调用这个Initialize方法对只读属性Bar进行赋值。在调用该方法之前,DI容器会根据预先注册的类型映射和其他相关的注入信息初始化该方法的参数。
1: public class Foo
2: {
3: public IBar Bar{get; private set;}
4:
5: [Injection]
6: public Initialize(IBar bar)
7: {
8: this.Bar = bar;
9: }
10: }
三、实例演示:创建一个简易版的DI框架
上面我们对DI容器以及三种典型的依赖注入方式进行了详细介绍,为了让读者朋友们对此具有更加深入的理解,介绍我们通过简短的代码创建一个迷你型的DI容器,即我们上面提到过的Cat。在正式对Cat的设计展开介绍之前,我们先来看看Cat在具体应用程序中的用法。
1: public interface IFoo {}
2: public interface IBar {}
3: public interface IBaz {}
4: public interface IQux {}
5:
6: public class Foo : IFoo
7: {
8: public IBar Bar { get; private set; }
9:
10: [Injection]
11: public IBaz Baz { get; set; }
12:
13: public Foo() {}
14:
15: [Injection]
16: public Foo(IBar bar)
17: {
18: this.Bar = bar;
19: }
20: }
21:
22: public class Bar : IBar {}
23:
24: public class Baz : IBaz
25: {
26: public IQux Qux { get; private set; }
27:
28: [Injection]
29: public void Initialize(IQux qux)
30: {
31: this.Qux = qux;
32: }
33: }
34:
35: public class Qux : IQux {}
我们在一个控制台应用中按照如上的形式定义了四个服务类型(Foo、Bar、Baz和Qux),它们分别实现了各自的服务接口(IFoo、IBar、IBaz和IQux)。定义在Foo中的属性Bar和Baz,以及定义在Baz中的属性Qux是三个需要自动注入的依赖属性,我们采用的注入方式分别是构造器注入、属性注入和方法注入。
我们在作为应用入口的Main方法中编写了如下一段程序。如下面的代码片段所示,在创建了作为DI容器的Cat对象之后,我们调用它的Register<TFrom, TTo>()方法注册了服务类型和对应接口之间的匹配关系。然后我们调用Cat对象的GetService<T>()方法通过指定的服务接口类型IFoo得到对应的服务对象,为了确保相应的依赖属性均按照我们希望的方式被成功注入,我们将它们显式在控制台上。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Cat cat = new Cat();
6: cat.Register<IFoo, Foo>();
7: cat.Register<IBar, Bar>();
8: cat.Register<IBaz, Baz>();
9: cat.Register<IQux, Qux>();
10:
11: IFoo service = cat.GetService<IFoo>();
12: Foo foo = (Foo)service;
13: Baz baz = (Baz)foo.Baz;
14:
15: Console.WriteLine("cat.GetService<IFoo>(): {0}", service);
16: Console.WriteLine("cat.GetService<IFoo>().Bar: {0}", foo.Bar);
17: Console.WriteLine("cat.GetService<IFoo>().Baz: {0}", foo.Baz);
18: Console.WriteLine("cat.GetService<IFoo>().Baz.Qux: {0}", baz.Qux);
19: }
20: }
这段程序被成功执行之后会在控制台上产生如下所示的输出结果,这充分证明了作为DI容器的Cat对象不仅仅根据指定的服务接口IFoo创建了对应类型(Foo)的服务对象,而且直接依赖的两个属性(Bar和Baz)分别以构造器注入和属性注入的方式被成功初始化,间接依赖的属性(Baz的属性Qux)也以方法注入的形式被成功初始化。
1: cat.GetService<IFoo>(): Foo
2: cat.GetService<IFoo>().Bar: Bar
3: cat.GetService<IFoo>().Baz: Baz
4: cat.GetService<IFoo>().Baz.Qux: Qux
在对Cat容器的用法有了基本了解之后,我们来正式讨论它的总体设计和具体实现。我们首先来看看用来标识注入构造函数、注入属性和注入方法的InjectionAttribute特性的定义,如下面的代码片段所示,InjectionAttribute仅仅是一个单纯的标识特性,它的用途决定了应用该特性的目标元素的类型(构造函数、属性和方法)。
1: [AttributeUsage( AttributeTargets.Constructor|
2: AttributeTargets.Property|
3: AttributeTargets.Method,
4: AllowMultiple = = false)]
5: public class InjectionAttribute: Attribute {}
如下所示的是Cat类的完整定义。我们采用一个ConcurrentDictionary<Type, Type>类型的字段来存放服务接口和具体服务类型之间的映射关系,这样的映射关系通过调用Register方法实现。针对服务类型(服务接口类型或者具体服务类型均可)的服务对象提供机制实现在GetService方法中。
1: public class Cat
2: {
3: private ConcurrentDictionary<Type, Type> typeMapping = new ConcurrentDictionary<Type, Type>();
4:
5: public void Register(Type from, Type to)
6: {
7: typeMapping[from] = to;
8: }
9:
10: public object GetService(Type serviceType)
11: {
12: Type type;
13: if (!typeMapping.TryGetValue(serviceType, out type))
14: {
15: type = serviceType;
16: }
17: if (type.IsInterface || type.IsAbstract)
18: {
19: return null;
20: }
21:
22: ConstructorInfo constructor = this.GetConstructor(type);
23: if (null == constructor)
24: {
25: return null;
26: }
27:
28: object[] arguments = constructor.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
29: object service = constructor.Invoke(arguments);
30: this.InitializeInjectedProperties(service);
31: this.InvokeInjectedMethods(service);
32: return service;
33: }
34:
35: protected virtual ConstructorInfo GetConstructor(Type type)
36: {
37: ConstructorInfo[] constructors = type.GetConstructors();
38: return constructors.FirstOrDefault(c => c.GetCustomAttribute<InjectionAttribute>() != null)
39: ?? constructors.FirstOrDefault();
40: }
41:
42: protected virtual void InitializeInjectedProperties(object service)
43: {
44: PropertyInfo[] properties = service.GetType().GetProperties()
45: .Where(p => p.CanWrite && p.GetCustomAttribute<InjectionAttribute>() != null)
46: .ToArray();
47: Array.ForEach(properties, p =>p.SetValue(service, this.GetService(p.PropertyType)));
48: }
49:
50: protected virtual void InvokeInjectedMethods(object service)
51: {
52: MethodInfo[] methods = service.GetType().GetMethods()
53: .Where(m => m.GetCustomAttribute<InjectionAttribute>() != null)
54: .ToArray();
55: Array.ForEach(methods, m=>
56: {
57: object[] arguments = m.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
58: m.Invoke(service, arguments);
59: });
60: }
61: }
如上面的代码片段所示,GetService方法利用GetConstructor方法返回的构造函数创建服务对象。GetConstructor方法体现了我们采用的注入构造函数的选择策略:优先选择标注有InjectionAttribute特性的构造函数,如果不存在则选择第一个公有的构造函数。执行构造函数传入的参数是递归地调用GetService方法根据参数类型获得的。
服务对象被成功创建之后,我们分别调用InitializeInjectedProperties和InvokeInjectedMethods方法针对服务对象实施属性注入和方法注入。对于前者(属性注入),我们在以反射的方式得到所有标注了InjectionAttribute特性的依赖属性并对它们进行赋值,具体的属性值同样是以递归的形式调用GetService方法针对属性类型获得。至于后者(方法注入),我们同样以反射的方式得到所有标注有InjectionAttribute特性的注入方法后自动调用它们,传入的参数值依然是递归地调用GetService方法针对参数类型的返回值。
ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core中的依赖注入(2):依赖注入(DI)
ASP.NET Core中的依赖注入(3):服务注册与提取
ASP.NET Core中的依赖注入(4):构造函数的选择与生命周期管理
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【总体设计】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【解读ServiceCallSite】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】