前言 依赖注入(DI)和控制反转(IoC)在现代研发技术上已经不陌生了,而陌生的却是应用这门技术的很多工程师,网上的很多资料大多数都是讲解如何使用框架来实现,偏于执行层面,而我这篇文章则偏于概念,让你彻底理解他们两者的关系以及原理和场景。
但很多人则把这两个概念搞混淆了,通俗地说:控制反转(IoC)是一种设计理念,依赖注入(DI)是这种理念的实践。
所有代码都将使用 “用户管理” 的作为示例,目的是用最通用的例子说清楚最根本的问题。
控制反转(IoC) 何为控制反转?这是很多文章一开始的标题,我也不例外,让不懂的小伙伴也不需要到处找文章了。先看一段代码。
public class UserService { public int CreateUser ( User user) { } }
这段代码很简单,我要创建一个用户,但是我没写方法的内容。思考一下,我现在写一个业务逻辑的方法用于 CRUD,那我是不是得先知道我用什么来操作数据库?
我的可选方案有:
原生的 ADO.NET 找一个现成的 SqlHelper 使用微软的 EntityFramework 其他第三方框架 Dapper/PetaPoco/… 如果我选择 EntityFramework 的话,那我的代码就会变成这个样子:
public int CreateUser ( User user) { using ( var context = new UserDbContext ( ) ) { context. Users. Add ( user) ; return context. SaveChanges ( ) ; } }
这样的代码没有问题,但说不定哪一天,领导来一句:“需求改了,我们要用 Dapper 来做数据库操作”,我想你肯定已经去外面找家伙,恨不得一锥子砸死这xxxx的领导。
一个系统不可能只有这么一个方法逻辑,肯定大量充斥并显示地 new 对象。如果领导这么坚持,懵逼的你要么走人(让新来的给你擦屁股,一种不负责的表现),要么改(把自己逼疯的结果),你怎么办?
因此就出现了一种设计模式,叫 控制反转=策略模式
就是把实例化的结果暴露给调用方,而不是实现方。
怎么定义调用方和实现方?
实现这个方法的就是实现方。调用你这个方法的就是调用方。
就好比,你会关心 EF 里面的 Add 方法是怎么实现的吗?你只是在需要的时候调用这个 Add 方法就好了。
所以为了满足领导的需求,我们需要封装一下:
声明一个接口,定义实现的规范:
public interface IDbContext { int Insert < T > ( T item) ; }
然后再使用到我们的业务逻辑类中&#xff1a;
public class UserService { private readonly IDbContext _context; public UserService ( IDbContext context) { _context &#61; context; } public int CreateUser ( User user) { return _context. Insert ( user) ; } }
构造方法的参数用于暴露给调用方&#xff0c;寓意如下&#xff1a;
让调用方在使用我这个方法时&#xff0c;必须要给我一个实现了 IDbContext
接口的实例&#xff08;因为有一个构造方法参数&#xff09;&#xff0c;怎么实现的实现方就不管了。
就是把实例化的控制权移交给了调用方&#xff0c;这就是控制反转。
调用方在使用时就得这样&#xff1a;
var userService &#61; new UserService ( new EFDbContext ( ) ) ; userService. CreateUser ( new User ( ) ) ;
但聪明的朋友会慢慢发现&#xff0c;其实我这个调用方到时候也充斥着大量地 new
操作&#xff0c;虽然也可以通过刚才的控制反转把要实例的内容暴露给调用方&#xff0c;但问题来了&#xff0c;什么时候是个头&#xff1f;
也有很多聪明的朋友会回答&#xff1a;在表现层的时候给实例。这个答案倒没什么错&#xff0c;但如果我一个类里&#xff0c;定义了很多个构造方法的参数呢&#xff1f;
public class MyService { public MyService ( IDbContext context, IUserRepository userRepository, IEmailService emailService, IInfoManager manager . . . ) }
难不成你都自己 new 一次&#xff1f;
new MyService ( new EFDbContext ( ) , new UserRepository ( new EFDbContext ( ) ) , new EmailService ( new UserRepository ( new EFDbContext ( ) ) ) ,
我只有一个感想&#xff0c;只有 SB 才这样写。所以&#xff0c;就出现了依赖注入。
依赖注入 什么叫依赖&#xff1f;什么叫注入&#xff1f;
人依赖氧气才能活着&#xff1b;家庭里依赖父母、亲人、爱人&#xff1b;学习依赖老师&#xff1b;晋升依赖于领导的提拔&#xff1b;租房子依赖于收入等等。
回到最初的一段代码上&#xff1a;
public class UserService { private readonly IDbContext _context; public UserService ( IDbContext context) { _context &#61; context; } public int CreateUser ( User user) { return _context. Insert ( user) ; } }
我要操作数据库&#xff0c;依赖的是 IDbContext
这接口的实现对象。但是怎么实现的&#xff0c;我就不用管了 。
注入这个词怎么理解呢&#xff1f; 在汉语词典中的解释&#xff1a;
1.泵入、灌入或流入。 2.以气息传送。 3.使产生对某物的印象或得到逐渐灌输。 一般注入指的是液体&#xff0c;一层一层的流下去&#xff1b;而我们的架构也是一层一层的进行封装&#xff08;表现层->逻辑层->数据层&#xff09;&#xff1b;
如果团队里有不同的人&#xff0c;张三喜欢和啤酒&#xff0c;李四喜欢和白酒&#xff0c;于是乎&#xff0c;张三从第二层开始倒啤酒&#xff0c;李四从第四层开始倒白酒&#xff0c;试想一下&#xff0c;最底下的酒杯里的酒会是什么味道&#xff1f;
因此&#xff0c;我们规定好了&#xff0c;只能从最顶上倒酒&#xff0c;这样下面留下来的酒都是同一种味道&#xff08;如下图&#xff09;
而换成技术来说&#xff0c;这种容器就有 Autofac、Unit、Sprint 等等。
实现原理 通过这个图我们就知道了&#xff0c;在注入的一开始&#xff0c;我们需要有一个容器&#xff0c;容器里装好了我们的内容&#xff0c;有可能这个容器里的酒是混合型的。
所以&#xff0c;在程序上&#xff0c;所有的注入配置都必须写在程序启动的时候&#xff0c;例如 Mvc 里的 Global 里&#xff0c;AspNetCore 的 ConfigureServices 里。
这里面需要提到一个技术概念&#xff1a;服务和实例
我们把依赖的对象叫做服务&#xff0c;把这个服务所对应的对象叫做实例。
你去消费&#xff0c;服务员给你提供的一系列服务&#xff0c;但你并不知道这些服务背后他们经历的培训有多少&#xff0c;但你实现了你的目标&#xff0c;那就是享受服务&#xff0c;满足需求。
我们回到代码层面&#xff1a;
public class UserService { private readonly IDbContext _context; public UserService ( IDbContext context) { _context &#61; context; } }
而我们在最外层使用了 Autofac 技术
var builder &#61; new ContainerBuilder ( ) ; builder. RegisterType < EFDbContext > ( ) . As < IDbContext > ( ) ;
你可以理解成&#xff1a;当遇到 服务
时&#xff0c;容器查找会已经注册过的 实例
。
就好比你去西餐厅点一道麻婆豆腐&#xff0c;餐厅不提供这道菜&#xff0c;你就满足不了需求。
生命周期 当然依赖注入框架还解决了一个问题&#xff0c;就是对于服务声明周期的管理。声明周期这个就需要和 GC
的概念挂钩了&#xff0c;如果你人为的 new
各种实例&#xff0c;我想没几个人会去主动释放掉没有用处的实例吧&#xff1f;
因此&#xff0c;依赖注入框架就把声明周期管理加入其中&#xff0c;帮助提高了性能。基本的就是三种类型&#xff1a;瞬间的、范围的、单例的。
瞬间的 每一次的访问&#xff0c;实例都是不一样的。 张三每一次访问这个页面&#xff0c;拿到的结果都不同。
范围的 在同一次访问时&#xff0c;实例都是一样的。 只要是张三访问&#xff0c;不管访问多少次&#xff0c;这些实例都是一样的。
无论张三还是李四&#xff0c;实例都是一样的。
总结 现在是不是已经深入了解依赖注入和控制反转他们俩的关系&#xff0c;以及为什么的问题了&#xff1f;
我们可以通过下图进行一次总结&#xff1a;