深入浅出的理解依赖注入(DI)和控制反转(IoC)的原理

前言

依赖注入(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 各种实例,我想没几个人会去主动释放掉没有用处的实例吧?

因此,依赖注入框架就把声明周期管理加入其中,帮助提高了性能。基本的就是三种类型:瞬间的、范围的、单例的。

  • 瞬间的
    每一次的访问,实例都是不一样的。

张三每一次访问这个页面,拿到的结果都不同。

  • 范围的
    在同一次访问时,实例都是一样的。

只要是张三访问,不管访问多少次,这些实例都是一样的。

  • 单例的
    任何时候,实例都是一样的。

无论张三还是李四,实例都是一样的。

总结

现在是不是已经深入了解依赖注入和控制反转他们俩的关系,以及为什么的问题了?

我们可以通过下图进行一次总结:
依赖注入标准设计

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叫我 Teacher 周

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值