依赖注入的通俗讲解,设计低耦合的系统
依赖注入的通俗讲解,设计低耦合的系统
依赖注入是一种实现方式,其目的是为了构建低耦合的系统,我用一个简单的生活中的例子来描述为什么需要依赖注入,以及依赖注入的好处
先讲一讲概念:允许从类的外部注入依赖项,因此注入依赖项的类只需要知道一个协定(通常是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容器的主要功能:
- 动态创建、注入依赖对象
- 管理对象的生命周期
- 映射依赖关系