IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用“好莱坞原则”是应用程序以被动的方式实现对流程的定制。我们可以采用若干设计模式以不同的方式实现IoC,比如我们在上面介绍的模板方法、工厂方法和抽象工厂,接下来我们介绍一种更为有价值的IoC模式,即依赖注入(DI:Dependency Injection,以下简称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中的依赖注入(2):依赖注入(DI)相关推荐

  1. ASP.NET Core Web 应用程序系列(三)- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)...

    在上一章中主要和大家分享了在ASP.NET Core中如何使用Autofac替换自带DI进行构造函数的批量依赖注入,本章将和大家继续分享如何使之能够同时支持属性的批量依赖注入. 约定: 1.仓储层接口 ...

  2. ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)...

    在上一章中主要和大家分享在MVC当中如何使用ASP.NET Core内置的DI进行批量依赖注入,本章将继续和大家分享在ASP.NET Core中如何使用Autofac替换自带DI进行批量依赖注入. P ...

  3. 【懒人有道】在asp.net core中实现程序集注入

    前言 在asp.net core中,我巨硬引入了DI容器,我们可以在不使用第三方插件的情况下轻松实现依赖注入.如下代码: 1 // This method gets called by the run ...

  4. ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器)

    前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期.这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 正文 今 ...

  5. ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起

    我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内 ...

  6. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  7. ASP.NET Core 中的依赖注入

    什么是依赖注入 软件设计原则中有一个依赖倒置原则(DIP),为了更好的解耦,讲究要依赖于抽象,不要依赖于具体.而控制反转(Ioc)就是这样的原则的其中一个实现思路, 这个思路的其中一种实现方式就是依赖 ...

  8. ASP.NET Core中使用GraphQL - 第三章 依赖注入

    ASP.NET Core中使用GraphQL ASP.NET Core中使用GraphQL - 第一章 Hello World ASP.NET Core中使用GraphQL - 第二章 中间件 SOL ...

  9. .NET Core ASP.NET Core Basic 1-2 控制反转与依赖注入

    本节内容为控制反转与依赖注入 简介 控制反转IOC 这个内容事实上在我们的C#高级篇就已经有所讲解,控制反转是一种设计模式,你可以这样理解控制反转,假设有一个人他有一部A品牌手机,他用手机进行听歌.打 ...

  10. ASP.NET Core中使用IOC三部曲(二.采用Autofac来替换IOC容器,并实现属性注入)

    上一篇ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器) ,我们说过ASP.NET Core中自带的IOC容器是属于轻量级的,功能并不是很多,只是提供了基础 ...

最新文章

  1. 安全攻防技能——安全基础概念
  2. linux uvc 支持的设备,摄像头是否支持uvc
  3. hive导数据到mysql 自增主键出错_老大问我:“建表为啥还设置个自增 id ?用流水号当主键不正好么?”...
  4. 获取最大轮廓 opencv
  5. kali2 安装docker_Docker 装 kali 的全套操作
  6. CodeForces - 1408F Two Different(构造+分治)
  7. 论文浅尝 | 从 6 篇顶会论文看「知识图谱」领域最新研究进展 | 解读 代码
  8. Codeforces 295A. Greg and Array
  9. Mysql 两种情况下更新字段中部分数据的方法
  10. pytorch 入门学习反向传播-4
  11. 什么是hasLayout
  12. python负数错误异常类型_十七、深入Python异常处理
  13. 199.二叉树的右视图
  14. 编程福利:50本C语言电子书,你还怕没书看吗!
  15. 微信绑定的卡服务器,微信亲属卡有什么作用 微信亲属卡怎么绑定
  16. MPPT算法(恒定电压、扰动观察、电导增量)介绍与实现过程
  17. 毛星云Opencv之图像修补综合示例程序
  18. python字典的存储结构_Python 字典的存储结构
  19. 一句话,连上隔壁老王家的 WiFi !
  20. 苹果ios7越狱后Safari浏览器与天气闪退情况解决方法详解

热门文章

  1. SharePoint 检测页面编辑模式
  2. Linear and Discrete Optimization - Week 1
  3. linux sed给空文件首行插入_Linux系统中sed命令在文件某行前后添加内容的用法
  4. python bisect_Python中bisect的用法
  5. 19个语法助你打牢Python基础
  6. mysql数据库oem_Oracle 11gR2学习之二(创建数据库及OEM管理篇)
  7. 宝德服务器安装系统6,简洁强大的内部结构
  8. 怎么把文字变成图形_PPT 中实现文字矢量化
  9. mysql行格式化_MySQL数据格式化语句
  10. PTA基础编程题目集-7-15 计算圆周率