该系列共5篇文章,旨在以实战模式,在.net下的数据库
控制台程序设计模式
Framework Mvc程序架构
Framework WebApi程序mvc
Core Api程序app
分别实现依赖注入。
其中.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,或怎么在MVC、WebApi和Core Api分别实现依赖注入有很好的帮助效果。
因此,如下内容,配合源码食用效果更佳~
该项目主要实战为主,理论部分我会结合例子和代码,深刻浅出地阐述,若是你是:
只要你花上半个小时认真读完每一句话,我有信心这篇文章必定会对你有所帮助。
若是你是:
那么也欢迎阅读,虽然可能对你帮助并不大,可是欢迎提供宝贵的意见,有写的很差的地方能够互相交流~
下面开始第一章《理论知识+实战控制台程序实现AutoFac注入》
依赖,简单说就是,当一个类须要另外一个类协做来完成工做的时候就产生了依赖。
这也是耦合的一种形式,可是是不可避免的。
咱们能作的不是消灭依赖,而是让依赖关系更清晰、更易于控制。
举个例子,好比标准的三层架构模式
名称 | 职责 | 举例 |
---|---|---|
界面层(UI) | 负责展现数据 | StudentController |
业务逻辑层(BLL) | 负责业务逻辑运算 | StudentService |
数据访问层(DAL) | 负责提供数据 | StudentRepository |
数据访问层(DAL)代码:
/// <summary> /// 学生仓储 /// </summary> public class StudentRepository { public string GetName(long id) { return "学生张三";//造个假数据返回 } }
业务层(BLL)代码:
/// <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,抽象出所需方法:
/// <summary> /// 学生仓储interface /// </summary> public interface IStudentRepository { string GetName(long id); }
而后让StudentRepository去实现这个接口:
/// <summary> /// 学生仓储 /// </summary> public class StudentRepository : IStudentRepository { public string GetName(long id) { return "学生张三";//造个假数据返回 } }
如今咱们在StudentService里只依赖于IStudentRepository,之后的增删改查都经过IStudentRepository这个抽象来作:
/// <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,那么:
我只须要关注业务逻辑层面, 若是我须要从仓储层拿数据库的数据, 好比我须要根据Id获取学生实体, 那么我只须要去IStudentRepository找Get(long id)函数就能够了, 至于实现它的仓储怎么实现这个方法我彻底不用管, 你怎么从数据库拿数据不是我该关心的事情。
个人工做就是实现IStudentRepository接口的全部方法就好了, 简单而明确, 至于谁来调用我,我不用管。 IStudentRepository里有根据Id获取学生姓名的方法, 我实现了就行, 至于业务逻辑层拿这个名字干啥, 那不是我要关心的事情。
这样看的话是否是彼此的职责就清晰多了,更进一步再举个极端的例子:
好比程序员B是个实习生,成天划水摸鱼,技术停留在上个世纪,结果他写的仓储层读取数据库所有用的手写sql语句的方式,极难维护,后来被领导发现领了盒饭,公司安排了另外一个程序员C来重写仓储层,C这时不须要动其余代码,只须要新建一个仓储StudentNewRepository,而后实现以前的IStudentRepository,C使用Dapper或者EF,写完新的仓储层以后,剩下的只须要在StudentService里改一个地方就好了:
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,全称Inversion of Control,即“控制反转”,是一种设计原则,最先由Martin Fowler提出,由于其理论提出时间和成熟时间相对较晚,因此并无被包含在GoF的《设计模式》中。
DI,全称Dependency Injection,即依赖注入,是实现IoC的其中一种设计方法。
其特征是经过一些技巧,将依赖的对象注入到调用者当中。(好比把Repository注入到Service当中)
这里说的技巧目前主要指的就是引入容器,先把全部会产生依赖的对象统一添加到容器当中,好比StudentRepository和StudentService,把分配权限交给容器,当StudentService内部须要使用StudentRepository时,这时不该该让它本身new出来一个,而是经过容器,把StudentRepository注入到StudentService当中。
这就是名称“依赖注入”的由来。
这是个老生常谈的问题了,并且这两个名字常常在各类大牛和伪大牛的吹逼现场频繁出现 ,听的新手云里雾里,莫名感到神圣不可侵犯。那么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是一个开源的轻量级的DI容器,
也是.net下最受你们欢迎的实现依赖注入的工具之一,
经过AutoFac咱们能够很方便的实现一些DI的骚操做。
目标很简单,就是控制台程序启动后,将学生姓名打印出来。
程序启动流程是,控制台主程序调用Service层,Service层调用Repository层获取数据(示例项目的仓储层没有链接数据库,只是直接造个假数据返回)。
没有依赖注入的状况下,确定是主程序会new一个StudentService,StudentService里会new一个StudentRepository,如今引入依赖注入后,就不该该这么new出来了,而是经过容器注入,也就是容器会把StudentRepository自动注入到StudentService当中。
学生实体类StudentEntity:
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接口:
using Ray.EssayNotes.AutoFac.Model; namespace Ray.EssayNotes.AutoFac.Repository.IRepository { /// <summary>学生仓储interface</summary> public interface IStudentRepository { string GetName(long id); } }
StudentRepository仓储类:
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 "学生张三";//造个假数据返回 } } }
IStudentService接口
namespace Ray.EssayNotes.AutoFac.Service.IService { /// <summary> /// 学生逻辑处理interface /// </summary> public interface IStudentService { string GetStuName(long id); } }
StudentService类:
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; } } }
其中构造函数是一个有参的函数,参数是学生仓储,这个后面依赖注入时会用。
须要先经过Nuget导入Autofac包:
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>(); } } }
其中:
须要先Nuget导入AutoFac程序包:
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),找到后会试图实例化一个对象出来。
AutoFac容器在解析StudentService的时候,会调用StudentService的构造函数进行实例化。
AutoFac容器发现StudentService的构造函数须要一个IStudnetRepository类型的参数,因而会自动去容器内寻找,根据这个暴露类型找到对应的StudnetRepository后,自动将其注入到了StudentService当中
通过这几步,一个简单的基于依赖注入的程序就完成了。
咱们将控制台程序设置为启动项目,点击运行,如图调用成功:
若是把调试断点加在容器初始化函数里,能够很清晰的看到哪些对象被注册到了容器里:
使用控制台程序原本是为了突出容器的概念,可是容易形成一些误解,DI的最终形态能够参考源码里的Api项目和MVC项目,原本想按部就班,先第一章控制台引入容器的概念,而后第二章讲批量注册、注入泛型、生命周期域管理,第三章讲Api和MVC项目,最后两章讲下.net core的DI,可是这里仍是先说下吧:
实际上是不须要一个一个注册的,运用批量注册后容器内部的代码是这样的,能够直接批量注册全部的:
/// <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入口是这样的,直接在构造函数里写就好了:
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); } }
就是直接在构造函数里注入就能够了。
首先DI是一个设计模式(design pattern),其自己彻底不存在过不过分的问题,这彻底取决于用的人和怎么用。 另外,在.NET Core中,DI被提到了一个很重要的地位,若是想要了解.NET Core,理解DI是必不可少的。