剖析依赖注入与控制反转的关联
1 前言
依赖注入(DI)和控制反转(IoC)或许是部分开发人员有所耳闻却未能清晰理解的两个概念,常常能看到它们的名称,却不明白其内涵。这两个词是英文的直译,看似高深,实则十分简单,并且在一些开发场景中有着不可或缺的作用,例如单元测试离不开依赖注入,IoC容器是插件框架的得力伙伴等,本文尝试以最为简洁的方式阐释这两种思想在开发中的应用。
2 什么是依赖注入与控制反转
2.1 控制反转
在解释控制反转之前,首先要理解“正转”的含义:当A依赖于B,且A掌控着B的创建与销毁时,此时A对B实施控制,这便是“正转”。
当B的创建与销毁并非由A内部完成,B脱离了A的掌控,这种情况就被称作控制反转(IoC:Invertion of Control)。
public class A
{
private B _b;
public A()
{
// 由于A掌控B的创建,所以A控制着B,此为“正转”
_b = new B();
}
}
2.2 依赖注入
对象之间的依赖不再由自身内部进行创建,而是由外部进行传递,这被称为依赖注入(DI:Dependency Injection)。
控制反转是一种设计理念,依赖注入是实现该理念的手段。二者缺一不可:
public class A
{
private B _b;
// B由外部注入,这就是依赖注入
public A(B b)
{
// B由外部创建,脱离了A的控制,这就是控制反转
_b = b;
}
}
上述代码示例的是构造函数注入,另一种常见的依赖注入方式是属性注入:
public class A
{
public B B { get; set; }
}
void main()
{
A a = new A();
B b = new B();
// 属性注入
a.B = b;
}
3 为什么要使用依赖注入与控制反转
3.1 解耦
在软件领域,有一条黄金法则叫做“高内聚,低耦合”。耦合意味着使用(或者说依赖),例如B使用了A,就表示B耦合了A,一旦类的数量增多,类之间错综复杂的耦合关系会成为巨大的挑战,高内聚就是将相同的功能汇聚在一起,这样类之间的耦合关系就会减少,通过提高内聚来降低类之间的耦合是一种常见的解耦方式。就像图中所示,C依赖B,B依赖A,原本是两级依赖关系,通过将B中的部分功能向A进行内聚(前提是这部分功能原本就具有相关性),实现了B、C都依赖于A的一级依赖关系,使得B、C之间完成了解耦。
解耦除了完全消除依赖关系之外,另一种方式是将紧耦合转变为松耦合。先解释一下松紧耦合的概念,打开电脑机箱找到主板上的南北桥芯片,会发现它们是直接焊接在主板上的,这种不可替换的连接就是紧耦合;再找到内存条,会发现它们可以拆卸并且更换为其他品牌,这种可替换的连接就是松耦合。大部分时候,在软件设计开发时都应当采用松散耦合,而依赖注入是实现松散耦合非常好的一种方式。
如果进一步思考,主板上的内存条之所以能够安装不同品牌,是因为存在相关技术标准,比如长宽尺寸、针脚数量、通信标准等,不同的内存条厂商,只要按照标准生产出来的内存条就能安装到同一块主板上。在软件开发中,让主板支持不同厂商的内存条就是可扩展性,定义内存条的接口标准就是抽象,按照标准生产内存条就是面向抽象编程(或者说面向接口编程)。因此,为了使软件模块具备更好的扩展性,除了使用依赖注入,还应当注入抽象而非具体的内容。
3.2 单元测试
不了解单元测试的小伙伴可以先阅读我的另一篇文章《单元测试从入门到精通》。在单元测试中如果没有依赖注入,几乎难以开展。通过简单的代码来举例:
难以测试的代码:
// 被测对象
public class House
{
private Bedroom _bedroom;
House()
{
// 内部构造协作对象,难以进行测试。
_bedroom = new Bedroom();
}
// ...
}
// 测试用例
public void TestThisIsReallyHard()
{
House house = new House();
// 无法在测试过程中对Bedroom进行属性赋值、行为方法调用等,测试难以进行
// ...
}
易于测试的代码:
// 被测对象
public class House
{
private Bedroom _bedroom;
// 注入协作对象,可测试性良好。
House(Bedroom b)
{
_bedroom = b;
}
// ...
}
// 测试用例
public void TestThisIsEasyAndFlexible()
{
// Bedroom对象处于掌控之中,易于测试
Bedroom bedroom = new Bedroom();
House house = new House(bedroom);
// ...
}
4 IoC容器
在稍复杂的软件产品中,通常会遇到两个关于对象的问题:一是对象数量众多,如何统一对它们进行管理,比如统一管理对象的创建销毁过程、每个对象的生命周期;二是对象之间可能存在多重复杂的依赖关系,如何对这些依赖关系进行管理,比如谁先创建谁后创建,被依赖的对象如何注入依赖对象等。
针对上述两个问题的解决方案就是IoC容器(IoC Container),IoC容器是一个对象管理器,它统一管理对象的创建销毁过程、生命周期、依赖关系,并且提供自动注入、根据配置创建对象等一系列便捷功能。如下代码使用 Autofac(C#开源IoC容器)进行了简单示例:
// 使用开源IoC容器Autofac
using Autofac;
namespace AutofacDemo
{
class A
{ }
class B
{
A _a;
// 只需要声明需要注入的对象,由容器自动完成依赖对象的创建与注入
public B(A a)
{
_a = a;
}
}
internal class Program
{
static void Main(string[] args)
{
// 将类型注册至容器中
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<A>();
// 设置对象的生命周期(单例模式)
builder.RegisterType<B>().SingleInstance();
// 构造IoC容器
IContainer container = builder.Build();
// 从容器中获取对象
B b = container.Resolve<B>();
}
}
}
5 结束语
依赖注入与控制反转的思想诞生于软件开发追求高内聚、低耦合的历史进程中,20世纪90年代末已在软件设计模式、单元测试中得到应用。2002年Java的Spring框架搭载着IoC容器、AOP等强大功能风靡全球,DI与IoC被更多的开发者所关注。直到最近的项目中涉及插件化框架,而IoC容器又是插件架构的最佳拍档,因此将其整理成文。若能对大家有所帮助,深感荣幸。
<全文完>