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