剖析依赖注入与控制反转的关联

剖析依赖注入与控制反转的关联

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容器又是插件架构的最佳拍档,因此将其整理成文。若能对大家有所帮助,深感荣幸。

<全文完>

版权声明:程序员胖胖胖虎阿 发表于 2025年6月18日 下午9:16。
转载请注明:

剖析依赖注入与控制反转的关联

| 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...