浅谈(IOC)依赖注入与控制反转(DI)

前言:参考了百度文献和https://www.cnblogs.com/liuqifeng/p/11077592.html以及http://www.cnblogs.com/leoo2sk/archive/2009/06/17/1504693.html(推荐)两篇文章,

一、依赖注入与控制反转的概念

       常用的IoC Container(容器)

    

依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

依赖注入控制反转的一种具体实现方式,那什么又是控制反转:它是面向对象编程中的一种设计原则,其作用主要用来减低计算机代码之间的耦合度,

  其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

 理解依赖注入:就是把依赖部分(代码不可控或者经常变动的耦合的部分)变成一个抽象的成员(类、抽象类或接口),然后根据具体所需要的实例去灵活的注入依赖,来达到控制反转的效果,从而实现代码解耦。

C#常用的依赖注入方式

  1,通过构造器进行依赖注入

  2,通过属性的访问器进行依赖注入

  3,通过接口实现依赖注入

  4,通过反射,特性也可以实现依赖注入

Setter注入是指在客户类中,设置一个服务类接口类型的数据成员,并设置一个Set方法作为注入点,这个Set方法接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。

构造注入(Constructor Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。

依赖获取(Dependency Locate)是指在系统中提供一个获取点,客户类仍然依赖服务类的接口。当客户类需要服务类时,从获取点主动取得指定的服务类,具体的服务类类型由获取点的配置决定。

可以看到,这种方法变被动为主动,使得客户类在需要时主动获取服务类,而将多态性的实现封装到获取点里面。获取点可以有很多种实现,也许最容易想到的就是建立一个Simple Factory作为获取点,客户类传入一个指定字符串,以获取相应服务类实例。如果所依赖的服务类是一系列类,那么依赖获取一般利用Abstract Factory模式构建获取点,然后,将服务类多态性转移到工厂的多态性上,而工厂的类型依赖一个外部配置,如XML文件。

不过,不论使用Simple Factory还是Abstract Factory,都避免不了判断服务类类型或工厂类型,这样系统中总要有一个地方存在不符合OCP的if…else或switch…case结构,这种缺陷是Simple Factory和Abstract Factory以及依赖获取本身无法消除的,而在某些支持反射的语言中(如C#),通过将反射机制的引入彻底解决了这个问题

IoC Container

说到依赖注入的话,就不能不提到IoC Container(IoC容器),那么到底什么是IoC容器?我们还是先来看看它的出现背景。

我们知道,软件开发领域有句著名的论断:不要重复发明轮子!因为软件开发讲求复用,所以,对于应用频繁的需求,总是有人设计各种通用框架和类库以减轻人们的开发负担。例如,数据持久化是非常频繁的需求,于是各种ORM框架应运而生;再如,对MVC的需求催生了Struts等一批用来实现MVC的框架。

随着面向对象分析与设计的发展和成熟,OOA&D被越来越广泛应用于各种项目中,然而,我们知道,用OO就不可能不用多态性,用多态性就不可能不用依赖注入,所以,依赖注入变成了非常频繁的需求,而如果全部手工完成,不但负担太重,而且还容易出错。再加上反射机制的发明,于是,自然有人开始设计开发各种用于依赖注入的专用框架。这些专门用于实现依赖注入功能的组件或框架,就是IoC Container。

从这点看,IoC Container的出现有其历史必然性。目前,最著名的IoC也许就是Java平台上的Spring框架的IoC组件,而.NET平台上也有Spring.NET和Unity等。

IoC Container 的分类

前面曾经讨论了三种依赖注入方式,但是,想通过方式对IoC Container进行分类很困难,因为现在IoC Container都设计很完善,几乎支持所有依赖注入方式。不过,根据不同框架的特性和惯用法,还是可以讲IoC Container分为两个大类。

    • 重量级IoC Container
      所谓重量级IoC Container,是指一般用外部配置文件(一般是XML)作为依赖源,并托管整个系统各个类的实例化的IoC Container。这种IoC Container,一般是承接了整个系统几乎所有多态性的依赖注入工作,并承接了所有服务类的实例化工作,而且这些实例化依赖于一个外部配置文件,这种IoC Container,很像通过一个文件,定义整个系统多态结构,视野宏大,想要很好驾驭这种IoC Container,需要一定的架构设计能力和丰富的实践经验。

      Spring和Spring.NET是重量级IoC Container的例子。一般来说,这种IoC Container稳定性有余而活性不足,适合进行低活多态性的依赖注入。
    • 轻量级IoC Container

      还有一种IoC Container,一般不依赖外部配置文件,而主要使用传参的Setter或Construtor注入,这种IoC Container叫做轻量级IoC Container。这种框架很灵活,使用方便,但往往不稳定,而且依赖点都是程序中的字符串参数,所以,不适合需要大规模替换和相对稳定的低活多态性,而对于高活多态性,有很好的效果。

      Unity是一个典型的轻量级IoC Container。

 下面引用https://www.cnblogs.com/RayWang/p/11128554.html 文章运用IOC容器AuotoFac

其中.Net Framework框架主要以如何引入AutoFac作为容器以及如何运用AuotoFac为主,.Net Core框架除了研究引入AutoFac的两种方式,同时也运用反射技巧对其自带的DI框架进行了初步封装,实现了相同的依赖注入效果。
项目架构如下图:

项目 名称 类型 框架
Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc Core容器 类库 .NET Core 2.2
Ray.EssayNotes.AutoFac.Infrastructure.Ioc Framework容器 类库 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Model 实体层 类库 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Repository 仓储层 类库 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Service 业务逻辑层 类库 .NET Framework 4.5
Ray.EssayNotes.AutoFac.ConsoleApp 控制台主程序 控制台项目 .NET Framework 4.5
Ray.EssayNotes.AutoFac.CoreApi Core WebApi主程序 Core Api项目 .NET Core 2.2
Ray.EssayNotes.AutoFac.NetFrameworkApi Framework WebApi主程序 Framework WebApi项目 .NET Framework 4.5
Ray.EssayNotes.AutoFac.NetFrameworkMvc Framework MVC主程序 Framework MVC项目 .NET Framework 4.5

GitHub源码地址:https://github.com/WangRui321/Ray.EssayNotes.AutoFac




DI理论基础#

依赖#

依赖,简单说就是,当一个类需要另一个类协作来完成工作的时候就产生了依赖。

这也是耦合的一种形式,但是是不可避免的。

我们能做的不是消灭依赖,而是让依赖关系更清晰、更易于控制。

举个例子,比如标准的三层架构模式

名称 职责 举例
界面层(UI) 负责展示数据 StudentController
业务逻辑层(BLL) 负责业务逻辑运算 StudentService
数据访问层(DAL) 负责提供数据 StudentRepository

数据访问层(DAL)代码:

Copy
    /// <summary>
    /// 学生仓储 /// </summary> public class StudentRepository { public string GetName(long id) { return "学生张三";//造个假数据返回 } }

业务层(BLL)代码:

Copy
    /// <summary>
    /// 学生逻辑处理 /// </summary> public class StudentService { private readonly StudentRepository _studentRepository; public StudentService() { _studentRepository = new StudentRepository(); } public string GetStuName(long id) { var stu = _studentRepository.Get(id); return stu.Name; } }

其中,StudentService的实现,就必须要依赖于StudentRepository。

而且这是一种紧耦合,一旦StudentRepository有更改,必然导致StudentService的代码同样也需要更改,如果改动量特别大话,这将是程序员们不愿意看到的。

面向接口#

面向是为了实现一个设计原则:要依赖于抽象,而不是具体的实现

还拿上面的例子说明,现在我们添加一个DAL的接口层,IStudentRepository,抽象出所需方法:

Copy
    /// <summary>
    /// 学生仓储interface /// </summary> public interface IStudentRepository { string GetName(long id); }

然后让StudentRepository去实现这个接口:

Copy
    /// <summary>
    /// 学生仓储 /// </summary> public class StudentRepository : IStudentRepository { public string GetName(long id) { return "学生张三";//造个假数据返回 } }

现在我们在StudentService里只依赖于IStudentRepository,以后的增删改查都通过IStudentRepository这个抽象来做:

Copy
    /// <summary>
    /// 学生逻辑处理 /// </summary> public class StudentService { private readonly IStudentRepository _studentRepository; public StudentService() { _studentRepository = new StudentRepository(); } public string GetStuName(long id) { var stu = _studentRepository.Get(id); return stu.Name; } }

这样做的好处有两个,一个是低耦合,一个是职责清晰。

如果对此还有怀疑的话,我们可以想象一个情景,就是负责写StudentService的是程序员A,负责写StudentRepository的是另一个程序员B,那么:

  • 针对程序员A
Copy
我只需要关注业务逻辑层面,
如果我需要从仓储层拿数据库的数据,
比如我需要根据Id获取学生实体,
那么我只需要去IStudentRepository找Get(long id)函数就可以了,
至于实现它的仓储怎么实现这个方法我完全不用管,
你怎么从数据库拿数据不是我该关心的事情。
  • 针对程序员B
Copy
我的工作就是实现IStudentRepository接口的所有方法就行了,
简单而明确,
至于谁来调用我,我不用管。
IStudentRepository里有根据Id获取学生姓名的方法,
我实现了就行,
至于业务逻辑层拿这个名字干啥,
那不是我要关心的事情。

这样看的话是不是彼此的职责就清晰多了,更进一步再举个极端的例子:

比如程序员B是个实习生,整天划水摸鱼,技术停留在上个世纪,结果他写的仓储层读取数据库全部用的手写sql语句的方式,极难维护,后来被领导发现领了盒饭,公司安排了另一个程序员C来重写仓储层,C这时不需要动其他代码,只需要新建一个仓储StudentNewRepository,然后实现之前的IStudentRepository,C使用Dapper或者EF,写完新的仓储层之后,剩下的只需要在StudentService里改一个地方就行了:

Copy
        public StudentService()
        {
            _studentRepository = new StudentNewRepository(); }

是不是职责清晰多了。

其实对于这个小例子来说,面向接口的优势还不太明显,但是在系统层面优势就会被放大。

比如上面换仓储的例子,虽然职责是清晰了,但是项目里有几个Service就需要改几个地方,还是很麻烦。

原因就是上面讲的,这是一种依赖关系,Service要依赖Repository,有没有一种方法可以让这种控制关系反转过来呢?当Service需要使用Repository,有没有办法让我需要的Repository自己注入到我这里来?

当然有,这就是我们将要实现的依赖注入。

使用依赖注入后你会发现,当C写完新的仓储后,业务逻辑层(StudentService)是不需要改任何代码的,所有的Service都不需要一个一个去改,直接在注入的时候修改规则,不要注入以前老的直接注入新的仓储就可以了。

面向接口后的架构:

名称 职责 举例
界面层(UI) 负责展示数据 StudentController
业务逻辑抽象层(InterfaceBLL) 业务逻辑运算抽象接口 IStudentService
业务逻辑层(BLL) 负责业务逻辑运算 StudentService
数据访问抽象层(InterfaceDAL) 数据访问抽象接口 IStudentRepository
数据访问层(DAL) 负责提供数据 StudentRepository

什么是IoC#

IoC,全称Inversion of Control,即“控制反转”,是一种设计原则,最早由Martin Fowler提出,因为其理论提出时间和成熟时间相对较晚,所以并没有被包含在GoF的《设计模式》中。

什么是DI#

DI,全称Dependency Injection,即依赖注入,是实现IoC的其中一种设计方法。

其特征是通过一些技巧,将依赖的对象注入到调用者当中。(比如把Repository注入到Service当中)

这里说的技巧目前主要指的就是引入容器,先把所有会产生依赖的对象统一添加到容器当中,比如StudentRepository和StudentService,把分配权限交给容器,当StudentService内部需要使用StudentRepository时,这时不应该让它自己new出来一个,而是通过容器,把StudentRepository注入到StudentService当中。

这就是名称“依赖注入”的由来。

DI和IoC有什么区别#

这是个老生常谈的问题了,而且这两个名字经常在各种大牛和伪大牛的吹逼现场频繁出现 ,听的新手云里雾里,莫名感到神圣不可侵犯。那么DI和IoC是同一个东西吗?如果不是,它们又有什么区别呢?

回答很简单:不是一个东西

区别也很简单,一句话概括就是:IoC是一种很宽泛的理念,DI是实现了IoC的其中一种方法

说到这里我已经感觉到屏幕后的你性感地添了一下嘴唇,囤积好口水,准备开始喷我了。

先别慌,我有证据,我们先来看下微软怎么说:

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.

地址:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2

翻译过来就是“ASP.NET Core支持依赖注入(DI)的软件设计模式,该模式是一种在类和它依赖的对象之间实现了控制反转(IoC)的技术”。

如果有人觉得辣鸡微软不够权威,那我们去看下IoC以及DI这两个概念的发明人——Martin Fowler怎么说:

几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了控制反转。这样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好象在说我的轿车是与众不同的,因为它有四个轮子。
因此,我想我们需要给这个模式起一个更能说明其特点的名字——”控制反转”这个名字太泛了,常常让人有些迷惑。经与多位IoC 爱好者讨论之后,我们决定将这个模式叫做”依赖注入”(Dependency Injection)。

地址:http://insights.thoughtworkers.org/injection/

Martin Fowler说的比较委婉,其实说白了就是建议我们,不要乱用IoC装逼,IoC是一种设计理念,很宽泛,你把程序里的一个写死的变量改成从配置文件里读取也是一种控制反转(由程序控制反转为由框架控制),你把这个配置改成用户UI界面的一个输入文本框由用户输入也是一种控制反转(由框架控制反转为由用户自己控制

所以,如果确定讨论的模式是DI,那么就表述为DI,还是尽量少用IoC这种宽泛的表达。

AutoFac#

AutoFac是一个开源的轻量级的DI容器,
也是.net下最受大家欢迎的实现依赖注入的工具之一,
通过AutoFac我们可以很方便的实现一些DI的骚操作。

实战控制台程序依赖注入#

目标很简单,就是控制台程序启动后,将学生姓名打印出来。
程序启动流程是,控制台主程序调用Service层,Service层调用Repository层获取数据(示例项目的仓储层没有连接数据库,只是直接造个假数据返回)。
没有依赖注入的情况下,肯定是主程序会new一个StudentService,StudentService里会new一个StudentRepository,现在引入依赖注入后,就不应该这么new出来了,而是通过容器注入,也就是容器会把StudentRepository自动注入到StudentService当中。

架构#

实体层#

学生实体类StudentEntity:

Copy
namespace Ray.EssayNotes.AutoFac.Model
{
    /// <summary>学生实体</summary> public class StudentEntity { /// <summary>唯一标识</summary> public long Id { get; set; } /// <summary>姓名</summary> public string Name { get; set; } /// <summary>成绩</summary> public int Grade { get; set; } } }

仓储层#

IStudentRepository接口:

Copy
using Ray.EssayNotes.AutoFac.Model;

namespace Ray.EssayNotes.AutoFac.Repository.IRepository
{
    /// <summary>学生仓储interface</summary> public interface IStudentRepository { string GetName(long id); } }

StudentRepository仓储类:

Copy
using Ray.EssayNotes.AutoFac.Model;
using Ray.EssayNotes.AutoFac.Repository.IRepository;

namespace Ray.EssayNotes.AutoFac.Repository.Repository
{
    /// <summary> /// 学生仓储 /// </summary> public class StudentRepository : IStudentRepository { public string GetName(long id) { return "学生张三";//造个假数据返回 } } }

Service层#

IStudentService接口

Copy
namespace Ray.EssayNotes.AutoFac.Service.IService
{
    /// <summary> /// 学生逻辑处理interface /// </summary> public interface IStudentService { string GetStuName(long id); } }

StudentService类:

Copy
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Service.IService;

namespace Ray.EssayNotes.AutoFac.Service.Service { /// <summary> /// 学生逻辑处理 /// </summary> public class StudentService : IStudentService { private readonly IStudentRepository _studentRepository; /// <summary> /// 构造注入 /// </summary> /// <param name="studentRepository"></param> public StudentService(IStudentRepository studentRepository) { _studentRepository = studentRepository; } public string GetStuName(long id) { var stu = _studentRepository.Get(id); return stu.Name; } } } 

其中构造函数是一个有参的函数,参数是学生仓储,这个后面依赖注入时会用。

AutoFac容器#

需要先通过Nuget导入Autofac包:

Copy
using System;
using System.Reflection;
//
using Autofac;
using Autofac.Core; // using Ray.EssayNotes.AutoFac.Repository.IRepository; using Ray.EssayNotes.AutoFac.Repository.Repository; using Ray.EssayNotes.AutoFac.Service.IService; using Ray.EssayNotes.AutoFac.Service.Service; namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc { /// <summary> /// 控制台程序容器 /// </summary> public static class Container { /// <summary> /// 容器 /// </summary> public static IContainer Instance; /// <summary> /// 初始化容器 /// </summary> /// <returns></returns> public static void Init() { //新建容器构建器,用于注册组件和服务 var builder = new ContainerBuilder(); //自定义注册 MyBuild(builder); //利用构建器创建容器 Instance = builder.Build(); } /// <summary> /// 自定义注册 /// </summary> /// <param name="builder"></param> public static void MyBuild(ContainerBuilder builder) { builder.RegisterType<StudentRepository>().As<IStudentRepository>(); builder.RegisterType<StudentService>().As<IStudentService>(); } } } 

其中:

  • public static IContainer Instance
    为单例容器
  • Init()方法
    用于初始化容器,即往容器中添加对象,我们把这个添加的过程称为注册(Register)。
    ContainerBuilder为AutoFac定义的容器构造器,我们通过使用它往容器内注册对象。
  • MyBuild(ContainerBuilder builder)方法
    我们具体注册的实现函数。RegisterType是AutoFac封装的一种最基本的注册方法,传入的泛型(StudentService)就是我们欲添加到容器的对象;As函数负责绑定注册对象的暴露类型,一般是以其实现的接口类型暴露,这个暴露类型是我们后面去容器内查找对象时使用的搜索标识,我们从容器外部只有通过暴露类型才能找到容器内的对象。

主程序#

需要先Nuget导入AutoFac程序包:

Copy
using System;
//
using Autofac;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc; using Ray.EssayNotes.AutoFac.Service.IService; namespace Ray.EssayNotes.AutoFac.ConsoleApp { class Program { static void Main(string[] args) { Container.Init();//初始化容器,将需要用到的组件添加到容器中 PrintStudentName(10001); Console.ReadKey(); } /// <summary> /// 输出学生姓名 /// </summary> /// <param name="id"></param> public static void PrintStudentName(long id) { //从容器中解析出对象 IStudentService stuService = Container.Instance.Resolve<IStudentService>(); string name = stuService.GetStuName(id); Console.WriteLine(name); } } }

进入Main函数,先调用容器的初始化函数,该函数执行成功后,StudentRepository和StudentService就被注册到容器中了。
然后调用打印学生姓名的函数,其中Resolve()方法是AutoFac封装的容器的解析方法,传入的泛型就是之前注册时的暴露类型,下面可以详细看下这一步到底发生了哪些事情:

  • 容器根据暴露类型解析对象

也就是容器会根据暴露类型IStudentService去容器内部找到其对应类(即StudentService),找到后会试图实例化一个对象出来。

  • 实例化StudentService

AutoFac容器在解析StudentService的时候,会调用StudentService的构造函数进行实例化。

  • 构造注入

AutoFac容器发现StudentService的构造函数需要一个IStudnetRepository类型的参数,于是会自动去容器内寻找,根据这个暴露类型找到对应的StudnetRepository后,自动将其注入到了StudentService当中

经过这几步,一个简单的基于依赖注入的程序就完成了。

结果#

我们将控制台程序设置为启动项目,点击运行,如图调用成功:

如果把调试断点加在容器初始化函数里,可以很清晰的看到哪些对象被注册到了容器里:

补充#

使用控制台程序本来是为了突出容器的概念,但是容易造成一些误解,DI的最终形态可以参考源码里的Api项目和MVC项目,本来想循序渐进,先第一章控制台引入容器的概念,然后第二章讲批量注册、注入泛型、生命周期域管理,第三章讲Api和MVC项目,最后两章讲下.net core的DI,但是这里还是先说下吧:

  • 误解1:每次添加Service和Repository都要去注册,不是更麻烦?

其实是不需要一个一个注册的,运用批量注册后容器内部的代码是这样的,可以直接批量注册所有的:

Copy
    /// <summary>
    /// .net framework MVC程序容器 /// </summary> public static class MvcContainer { public static IContainer Instance; /// <summary> /// 初始化容器 /// </summary> /// <param name="func"></param> /// <returns></returns> public static void Init(Func<ContainerBuilder, ContainerBuilder> func = null) { //新建容器构建器,用于注册组件和服务 var builder = new ContainerBuilder(); //注册组件 MyBuild(builder); func?.Invoke(builder); //利用构建器创建容器 Instance = builder.Build(); //将AutoFac设置为系统DI解析器 System.Web.Mvc.DependencyResolver.SetResolver(new AutofacDependencyResolver(Instance)); } public static void MyBuild(ContainerBuilder builder) { Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb(); //批量注册所有仓储 && Service builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes) .Where(cc => cc.Name.EndsWith("Repository") |//筛选 cc.Name.EndsWith("Service")) .PublicOnly()//只要public访问权限的 .Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型) .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口) //注册泛型仓储 builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)); //注册Controller Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkMvc")); builder.RegisterControllers(mvcAssembly); } }

误解2:每次使用都要解析下,还不如直接new
好吧,其实也是不需要自己去解析的,最终形态的Controller入口是这样的,直接在构造函数里写就行了:

Copy
    public class StudentController : Controller
    {
        private readonly IStudentService _studentService; public StudentController(IStudentService studentService) { _studentService = studentService; } /// <summary> /// 获取学生姓名 /// </summary> /// <param name="id"></param> /// <returns></returns> public string GetStuNameById(long id) { return _studentService.GetStuName(id); } }

就是直接在构造函数里注入就可以了。

  • 误解3:依赖注入是不是过度设计?

首先DI是一个设计模式(design pattern),其本身完全不存在过不过度的问题,这完全取决于用的人和怎么用。
另外,在.NET Core中,DI被提到了一个很重要的地位,如果想要了解.NET Core,理解DI是必不可少的。

猜你喜欢

转载自www.cnblogs.com/hudean/p/11677372.html