Decade
Decade
Published on 2025-10-09 / 7 Visits
0
0

依赖注入

.NET 依赖注入(Dependency Injection, DI)技术笔记

一、依赖倒转原则(Dependency Inversion Principle, DIP)

核心思想

高层模块不应该依赖低层模块,而应该依赖抽象。这是SOLID原则中的重要组成部分,它要求:

  • 高层模块不应该依赖具体实现

  • 低层模块不应该定义抽象

  • 抽象不应该依赖细节,细节应该依赖抽象

为什么要依赖倒转?

依赖倒转原则让我们能够:

  • 专注于单一类的业务逻辑本身,而不是纠结于对象如何创建

  • 通过依赖抽象而非具体实现,降低模块间的耦合度

  • 提高代码的可测试性和可维护性

  • 使得系统更容易扩展和修改

与控制反转的关系

依赖倒转原则是控制反转(IoC)思想的基础。DIP提供了理论指导,而IoC则是实现这一原则的具体方法论。


二、控制反转(IoC, Inversion of Control)

什么是控制反转?

控制反转是一种设计思想,它将对象创建与依赖关系的控制权交给外部容器,而不是由对象自己控制。传统方式中,对象自己负责创建或查找它所依赖的对象;而在IoC模式下,这些工作由外部容器完成。

两种典型实现方式

1. 服务定位器(Service Locator)

  • 提供一个中心化的注册表

  • 对象主动从服务定位器中查找所需的依赖

  • 存在隐式依赖,不够透明

2. 依赖注入(Dependency Injection, DI)

  • 对象被动接收依赖

  • 依赖通过构造函数、属性或方法注入

  • 依赖关系显式声明,更加透明

.NET 中的选择

.NET 中主要使用 DI 实现 IoC。从 .NET Core 开始,依赖注入已经成为框架的核心功能,内置了强大的 DI 容器。


三、.NET 中的依赖注入机制

1. 服务与接口

服务的概念

在 DI 上下文中,"服务"指的是需要被使用的对象,它可以是:

  • 业务逻辑组件

  • 数据访问层

  • 工具类

  • 任何可重用的功能模块

面向接口编程

一般通过接口定义服务类型,通过类实现具体逻辑

  • 接口定义契约和行为规范

  • 具体类实现业务逻辑

  • 依赖方只需要知道接口,不需要知道具体实现

服务注册

在 .NET 的 DI 容器中注册服务:

// 注册服务
services.AddTransient<IService, ServiceImpl>();
services.AddScoped<IRepository, SqlRepository>();
services.AddSingleton<ICache, MemoryCache>();

重点注册时使用接口类型,获取对象时也要使用接口。这是面向接口编程的核心原则,确保了代码的灵活性和可替换性。

2. 构造函数注入

默认注入方式

.NET 默认使用构造函数注入,这是最推荐的依赖注入方式:

public class OrderService
{
    private readonly IRepository _repository;
    private readonly ILogger _logger;
    
    public OrderService(IRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }
}

自动依赖解析

  • 如果类没有无参构造函数,容器会根据构造函数参数自动从已注册服务中提供依赖

  • DI 容器会递归解析所有依赖关系

  • 形成完整的依赖链

处理未注册的依赖

如果依赖中存在未注册项,可以通过以下方式处理:

  1. 手动创建实例

services.AddTransient<IService>(provider => new ServiceImpl("config"));
  1. 使用工厂模式

services.AddTransient<IService>(provider =>
{
    var logger = provider.GetService<ILogger>();
    return new ServiceImpl(logger, new CustomConfig());
});

四、服务的获取与管理

1. 获取单个服务

GetService<T>() 方法

var service = serviceProvider.GetService<IService>();
  • 如果服务未注册,会返回 null

  • 不会抛出异常,需要自行判断空值

GetRequiredService<T>() 方法

var service = serviceProvider.GetRequiredService<IService>();
  • 如果服务未注册,会抛出异常

  • 适用于必须存在的服务

多次注册的处理

当同一接口有多个实现被注册时,默认返回最后注册的实例

2. 获取多个服务

使用 GetServices<T>() 获取所有注册的服务集合:

var services = serviceProvider.GetServices<INotification>();
foreach (var service in services)
{
    service.Send();
}

这在实现插件模式或策略模式时特别有用。


五、依赖注入的"传染性"

什么是 DI 的传染性?

DI 具有"传染性"特征:

  • 第一个服务可能需要通过服务定位器方式获取

  • 但其构造函数中的依赖会被容器自动补全

  • 一旦开始使用 DI,整个对象图都会被 DI 容器管理

灵活性体现

通过接口注册 + 实例注入,可以灵活替换实现而不修改业务逻辑:

// 开发环境
services.AddTransient<IEmailService, MockEmailService>();

// 生产环境
services.AddTransient<IEmailService, SmtpEmailService>();

优点总结

  • 服务对象具有高度替代性:轻松切换实现

  • 降低耦合:依赖于抽象而非具体实现

  • 修改业务逻辑时无需改动主流程:只需替换具体实现

  • 提高可测试性:便于使用模拟对象进行单元测试

  • 增强可维护性:清晰的依赖关系,易于理解和维护


六、示例代码:构造函数集合注入

using GetAddress;
using System;
using System.Collections.Generic;

namespace SendService
{
    public class TheNew : ISendService
    {
        private readonly IEnumerable<IGetAddress> set;
        
        public TheNew(IEnumerable<IGetAddress> set)
        {
            this.set = set;
        }
        
        public void Send()
        {
            string info = "";
            foreach (IGetAddress item in set)
            {
                info = item.GetAddress();
            }
            Console.WriteLine(info + "发送");
        }
    }
}

代码说明

  • 集合注入:构造函数参数使用 IEnumerable<IGetAddress>

  • 自动装配:只要向容器注册了多个实现了 IGetAddress 的对象,容器就能自动注入整个集合

  • 灵活扩展:可以动态添加新的 IGetAddress 实现,无需修改 TheNew

  • 体现了 .NET DI 的灵活与自动化特性

注册示例

services.AddTransient<IGetAddress, EmailAddress>();
services.AddTransient<IGetAddress, SmsAddress>();
services.AddTransient<IGetAddress, PushAddress>();
services.AddTransient<ISendService, TheNew>();

七、总结

核心价值

  • 关注点分离:依赖注入让我们只关注业务逻辑,而非对象构建

  • 降低耦合:通过依赖抽象减少模块间的直接依赖

  • 提高可维护性:清晰的依赖关系和易于替换的实现

最佳实践

  • 面向接口编程是核心原则:始终通过接口定义服务契约

  • 优先使用构造函数注入:明确表达依赖关系

  • 合理选择生命周期:Transient、Scoped、Singleton

  • 避免服务定位器反模式:尽量使用依赖注入而非主动获取服务


Comment