依赖注入的通俗讲解,设计低耦合的系统

依赖注入的通俗讲解,设计低耦合的系统

依赖注入是一种实现方式,其目的是为了构建低耦合的系统,我用一个简单的生活中的例子来描述为什么需要依赖注入,以及依赖注入的好处

先讲一讲概念:允许从类的外部注入依赖项,因此注入依赖项的类只需要知道一个协定(通常是C#接口)

这句话很抽象,我们可以拿现实中的例子来对比

概念 类比
允许从类的外部注入依赖项 家里的供电线路接入电器
因此注入依赖项的类只需要知道一个协定(通常是C#接口) 不需要知道是什么电器,只要是对应的插孔接上就能用

这个概念不一定好理解,我们可以先用生活的例子说明依赖注入的好处,这里用一个厨房需要电器的例子

没有依赖注入/没有电源插头

修改的生活例子 具体操作
在厨房一角安装一台电饭锅 将电饭锅的电源线接入供电线,缠上绝缘胶布以防触电或短路
将电饭锅更换为微波炉 撕开之前缠上的绝缘胶布,拆除电饭锅的电源线,更换为微波炉的电源线,重新缠上绝缘胶布以防触电或短路
将微波炉更换为豆浆机 撕开之前缠上的绝缘胶布,拆除微波炉的电源线,更换为豆浆机的电源线,重新缠上绝缘胶布以防触电或短路
测试的生活例子 具体操作
测试不工作的豆浆机是供电线问题还是豆浆机问题 撕开之前缠上的绝缘胶布,拆除豆浆机的电源线,更换为其他电器的电源线,重新缠上绝缘胶布以防触电或短路,工作则是豆浆机问题,不工作则是供电线问题

是的,就是这么麻烦,修改一次需要电工过来,需要安装电器设备的过来,一起折腾一两个小时的时间,当需求变更,没有依赖注入/没有电源插头就是这么麻烦,所以我们总是要加班,加不完的班

但是如果有插头/依赖注入呢

拥有插头/依赖注入

修改的生活例子 具体操作
将电饭锅换为微波炉 取下电饭锅的插头,插上微波炉的插头
将微波炉换为豆浆机 取下微波炉的插头,插上豆浆机的插头
测试的生活例子 具体操作
测试不工作的豆浆机是供电线问题还是豆浆机问题 取下豆浆机的插头,插上一个其他正常电器,工作则是豆浆机问题,不工作则是供电线问题

此时问题就变得很简单,只需要安装电器设备的简单操作一下就解决了问题

这个问题换到软件开发也是一样的,如果没有插头和插座(接口),问题就变得异常复杂,实际上在软件开发过程中,真的有很多人不用插头和插座(接口)

class Boiler
{
    //这个方法可以简写为
    // public string Work() => "我是锅我在烧水";
    public string Work()
    {
        return "我是锅我在烧水";
    }
}
class Power
{
    public string DoWork()
    {
        Boiler boiler = new Boiler();
        return boiler.Work();
    }
}
class Program
{
    static void Main(string[] args)
    {
        Power power = new Power();
        Console.WriteLine(power.DoWork()); 
    }
}

这就是一个没有插头的例子,在Power这个供电线上,直接接上了锅,如果要有需求变更,则需要在供电线(Power类)上更改DoWork方法,同时在测试中,如果出现了问题,也很难定位到问题是处在Power还是Boiler

这只是一个简单的示例,只在一个地方用到了Boiler可能看起来好像也只需要更改一点点,但是在大型项目中,可能会几十次几百次的用到,那就费时费力了,要是有哪个地方没改到,问题就更严重了

于是在软件开发中有了依赖注入这一概念

interface IWork
{
    string Work();
}
class Boiler:IWork
{
    //这个方法可以简写为
    // public string Work() => "我是锅我在烧水";
    public string Work()
    {
        return "我是锅我在烧水";
    }
}
class Power
{
    private readonly IWork _work;
    public Power(IWork work)
    {
        //将work形参赋值给_work字段,如果work形参是null则抛出异常
        _work = work ?? throw new ArgumentNullException(nameof(work)); ;
    }
    public string DoWork()
    {
        return _work.Work();
    }
}
class Program
{
    static void Main(string[] args)
    {
        Power power = new Power(new Boiler());
        Console.WriteLine(power.DoWork()); 
    }
}

这个代码便是最基本的依赖注入,在这份代码中,首先定义了一个IWork的插头,Boiler满足IWork的插头要求

Power中,设计了一个_work插座,接收IWork插头,在使用时,Power供电给这个插头就行了

在这个设计中,如果需要将锅更换为其他电器,只需要在构造Power对象时传递实现了这个插头的其他类即可,无需再改动Power中的代码

还是一个表格对比差异

没有使用依赖注入

修改的需求 具体实现
A类调用B类的实例方法 在当前类中new一个对象执行
更换A中调用的B类的实例方法为C类的实例方法 修改A类中所有有关B类的引用为C类的引用(可能包含成百上千的引用,一旦漏掉就是严重的BUG)
更换A中调用的C类的实例方法为D类的实例方法 修改A类中所有有关C类的引用为D类的引用(可能包含成百上千的引用,一旦漏掉就是严重的BUG)
测试的需求 现实
A类工作出现严重异常 无法准确定位A类错误还是其调用的底层类错误

使用依赖注入

修改的需求 具体实现
A类调用B类的实例方法 创建一个接口,A类中调用符合接口的方法,构造A类对象时将B类的实例对象传递过去
更换A中调用的B类的实例方法为C类的实例方法 更改A类对象构造参数中B类实例对象为C类实例对象
更换A中调用的C类的实例方法为D类的实例方法 更改A类对象构造参数中C类实例对象为D类实例对象
测试的需求 现实
A类工作出现严重异常 编写简单的、不出错的简单类传递给A类测试,若A类正常则是底层类错误

使用依赖注入的好处是显而易见的,尤其是在需求频繁变更的时候。

0x02 IOC容器(DI容器)

依赖注入很好,但是大型项目中成百上千的依赖如果需要手动注入依然很麻烦,于是有了DI容器(IOC容器)

  • IOC:为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不再被依赖模块的类中直接通过new来获取

在使用依赖注入时就是使用了IOC设计原理,所以我们说的DI容器其实和IOC容器是一个东西,虽然他们之间的概念有一点差别

IOC容器的主要功能:

  1. 动态创建、注入依赖对象
  2. 管理对象的生命周期
  3. 映射依赖关系

IOC容器(DI容器)下一次单独拿出来写,没有IOC容器的依赖注入看不出使用依赖注入的巨大优势

posted @ 2020-07-28 08:38  吴俊城  阅读(199)  评论(0编辑  收藏  举报