C#中级39、什么是依赖注入设计模式

依赖注入(Dependency Injection, DI) 是一种控制反转(Inversion of Control, IoC) 的具体实现方式,它是一种设计模式 ,用于解耦对象之间的依赖关系 ,使代码更可测试、可维护、可扩展

✅ 一句话理解:
"不要自己 new 依赖,而是让别人(容器)把依赖'送'给你。"


🎯 为什么需要依赖注入?

❌ 传统写法的问题(紧耦合)

复制代码
public class OrderService
{
    private readonly EmailSender _emailSender = new EmailSender(); // 硬编码依赖
    
    public void PlaceOrder()
    {
        // ... 处理订单
        _emailSender.Send("订单已创建"); // 无法替换、无法测试
    }
}

问题

  • 难以测试 :无法 mock EmailSender(比如测试时不想真发邮件)
  • 难以修改 :想换成 SmsSender?必须改代码!
  • 违反开闭原则:对修改开放,对扩展封闭

✅ 依赖注入的核心思想

高层模块(如 OrderService)不直接创建低层模块(如 IEmailSender),而是通过接口 + 外部传入的方式获得依赖。

步骤:

  1. 定义抽象(接口)
  2. 依赖抽象,而非具体实现
  3. 由外部(DI 容器)负责创建并注入依赖

💡 三种依赖注入方式

1️⃣ 构造函数注入(推荐!)

复制代码
public interface IEmailSender
{
    void Send(string message);
}

public class SmtpEmailSender : IEmailSender { ... }
public class MockEmailSender : IEmailSender { ... } // 用于测试

// OrderService 不关心谁实现 IEmailSender
public class OrderService
{
    private readonly IEmailSender _emailSender;
    
    // 依赖通过构造函数"注入"
    public OrderService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
    
    public void PlaceOrder()
    {
        _emailSender.Send("订单已创建");
    }
}

2️⃣ 属性注入(Setter 注入)

复制代码
public class OrderService
{
    public IEmailSender EmailSender { get; set; } // 通过属性设置
    
    public void PlaceOrder() => EmailSender?.Send("...");
}

⚠️ 少用:依赖可能为 null,生命周期难控制。

3️⃣ 方法注入

复制代码
public void PlaceOrder(IEmailSender sender)
{
    sender.Send("...");
}

适用于临时依赖或策略模式。


🧩 DI 容器:自动管理依赖的"工厂"

你不需要手动 new OrderService(new SmtpEmailSender()),而是交给 DI 容器 自动完成:

在 .NET 中使用内置 DI(ASP.NET Core 默认支持)

1. 注册服务(Program.cs)
复制代码
var builder = WebApplication.CreateBuilder(args);

// 注册依赖:当需要 IEmailSender 时,给 SmtpEmailSender 实例
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<OrderService>(); // 自动注入 IEmailSender
2. 使用(Controller 或其他服务)
复制代码
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;
    
    // DI 容器自动创建 OrderService 并注入所需依赖
    public OrderController(OrderService orderService)
    {
        _orderService = orderService;
    }
    
    [HttpPost]
    public IActionResult CreateOrder()
    {
        _orderService.PlaceOrder();
        return Ok();
    }
}

🔁 依赖生命周期(.NET DI)

生命周期 说明 适用场景
Transient 每次请求都创建新实例 轻量、无状态服务
Scoped 每个 HTTP 请求(或作用域)一个实例 Entity Framework DbContext
Singleton 整个应用生命周期只有一个实例 配置、日志记录器
复制代码
services.AddTransient<IMyService, MyService>();      // 每次 new
services.AddScoped<IMyService, MyService>();         // 每请求一个
services.AddSingleton<IMyService, MyService>();      // 全局唯一

✅ 依赖注入的好处

优势 说明
解耦 高层模块不依赖具体实现,只依赖接口
可测试性 轻松用 mock 对象替换真实依赖(单元测试必备)
可维护性 修改/替换实现无需改动使用方代码
可扩展性 新增实现只需注册,无需修改业务逻辑
配置灵活 开发/测试/生产环境可注入不同实现

🆚 依赖注入 vs 服务定位器(Service Locator)

方式 依赖注入 服务定位器
代码 public MyClass(IService s) var s = ServiceLocator.GetService<IService>()
耦合度 低(显式声明依赖) 高(隐藏依赖,运行时才知道)
可测试性
推荐度 ✅ 强烈推荐 ❌ 反模式(隐藏依赖)

📌 依赖注入让依赖关系"显式化",这是良好设计的关键!


⚠️ 常见误区

❌ 误区 1:DI 就是用框架

DI 是设计思想,即使不用框架,手动传参也是 DI(只是麻烦)。

❌ 误区 2:所有类都要注入

简单的 DTO、实体类、工具类(如 DateTime.Now)不需要 DI。

❌ 误区 3:循环依赖

A 依赖 B,B 又依赖 A → 设计有问题!应重构(引入中介者或拆分职责)。


✅ 最佳实践

  1. 优先使用构造函数注入
  2. 依赖接口,而非具体类
  3. 避免在构造函数中做复杂逻辑(保持轻量)
  4. 合理选择生命周期(别把 Scoped 当 Singleton 用)
  5. 不要在静态方法中使用 DI(破坏可测试性)

🧠 总结

关键点 说明
本质 控制反转(IoC)的一种实现
核心 "不要自己 new,让别人给你"
方式 构造函数注入(首选)、属性注入、方法注入
工具 .NET 内置 DI 容器(IServiceCollection
价值 解耦、可测试、可维护、可扩展

💡 记住
"依赖注入不是为了用框架,而是为了写出干净、灵活、可测试的代码。"

问题:

什么是依赖注入框架?

他们是自动创建依赖项并且将他们注入到需要他们的对象中的机制。他们是可配置的

好处是什么

依赖注入将类与依赖项解耦

那总有一个地方new,那是谁new

这是个极其关键且深刻的问题!👏

你说得完全对:依赖不会凭空出现,总得有人 new 出来 。在依赖注入(DI)体系中,这个"谁"就是 DI 容器(Dependency Injection Container) ------ 也叫 IoC 容器(Inversion of Control Container)


✅ 答案一句话:

DI 容器负责 new 所有注册的服务,并自动把它们"注入"到需要的地方。

你写的代码不再手动 new 依赖 ,而是把"创建对象"的责任委托给 DI 容器


🔍 详细解释:DI 容器是如何工作的?

步骤 1️⃣:你在启动时注册依赖关系

复制代码
// Program.cs(ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);

// 告诉容器:"当我需要 IEmailSender 时,请给我一个 SmtpEmailSender 实例"
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

// 告诉容器:"OrderService 是一个服务,它的依赖由你自动解决"
builder.Services.AddScoped<OrderService>();

📌 这里你没有 new 任何对象 ,只是声明规则


步骤 2️⃣:当程序运行时,DI 容器按需创建对象

假设你的控制器这样写:

复制代码
public class OrderController : ControllerBase
{
    public OrderController(OrderService orderService) // ← 构造函数注入
    {
        // ...
    }
}

当用户发起 HTTP 请求时:

  1. 框架(如 ASP.NET Core)发现需要 OrderController

  2. 它问 DI 容器:"请给我一个 OrderController 实例"

  3. DI 容器检查 OrderController 的构造函数,发现它需要 OrderService

  4. 容器再检查 OrderService 的构造函数,发现它需要 IEmailSender

  5. 容器根据注册规则,执行:

    复制代码
    var emailSender = new SmtpEmailSender(); // ← 这里是容器在 new!
    var orderService = new OrderService(emailSender);
    var controller = new OrderController(orderService);
  6. 最终把完整的 OrderController 实例交给框架处理请求

💡 整个过程对你透明 ------ 你只写了接口和类,容器自动完成所有 new 和组装


🧩 谁是"根对象"?------ Composition Root(组合根)

在 DI 体系中,第一个被容器创建的对象 叫做 Composition Root(组合根)

  • ASP.NET Core 中:组合根是 Controller、Razor Page、Minimal API 处理函数
  • 控制台应用 中:通常是 Main 方法中手动从容器解析的第一个服务
  • WPF/WinForms 中:可能是 MainWindowApp

只有组合根是由框架或你显式从容器"拉取"(Resolve)的,其他所有依赖都是容器自动"推送"(Inject)的。


💻 控制台应用示例(手动触发 DI)

复制代码
// 1. 注册服务
var services = new ServiceCollection();
services.AddScoped<IEmailSender, SmtpEmailSender>();
services.AddScoped<OrderService>();

// 2. 构建容器
var serviceProvider = services.BuildServiceProvider();

// 3. 【关键】从容器"拉取"根对象(这里是你唯一接触容器的地方)
var orderService = serviceProvider.GetRequiredService<OrderService>();

// 4. 使用(orderService 内部的 SmtpEmailSender 已自动注入)
orderService.PlaceOrder();

🔸 GetRequiredService<T>() 就是那个"new"的起点 ------ 但它是容器在 new,不是你在 new


🌐 类比理解

角色 现实类比
你(开发者) 餐厅老板,只负责写菜单(定义接口和业务逻辑)
DI 容器 厨房总管,知道每道菜怎么做(new 对象),并自动配好配料
客户端代码 顾客,点菜(请求服务),不关心厨房怎么做的
Composition Root 服务员,从厨房端出完整菜品(根对象)

你不需要进厨房切菜(new SmtpEmailSender()),厨房总管(DI 容器)全包了!


⚠️ 重要原则:不要在业务代码中调用容器!

❌ 错误做法(服务定位器反模式):

复制代码
public class OrderService
{
    public void PlaceOrder()
    {
        // 不要这样做!隐藏了依赖,破坏可测试性
        var sender = serviceProvider.GetService<IEmailSender>();
        sender.Send("...");
    }
}

✅ 正确做法:

  • 只在 Composition Root(如 Program.cs、Startup)配置容器
  • 只在 Composition Root 或框架入口处解析根对象
  • 业务代码永远通过构造函数/参数接收依赖,绝不主动找容器要

✅ 总结

问题 答案
总得有人 new,是谁? DI 容器 (如 .NET 的 ServiceProvider
我在哪看到 new 看不到 ------ 容器在幕后自动完成
我需要手动 new 吗? 不需要(除了极少数 POCO 对象如 DTO)
第一个对象谁创建? 框架或你在 Composition Root 从容器解析
这有什么好处? 解耦、可测试、生命周期自动管理

🧠 记住
"你负责设计依赖关系,容器负责实现它。"

这就是依赖注入的精髓 ------ 把"创建"的控制权交出去,换来代码的灵活性和可维护性。

如果用prism框架呢?

非常好的问题!当你在使用 Prism 框架 (常用于 WPF、Uno Platform、MAUI 等 MVVM 应用)时,你已经站在一个成熟的依赖注入体系之上 。Prism 内置了对 DI 容器的支持,并帮你管理了"谁来 new"这个关键问题。


✅ Prism 中的 DI 机制概览

Prism 不强制绑定某个 DI 容器 ,而是通过抽象接口 IContainerExtension 支持多种容器,例如:

  • Unity(Prism 早期默认)
  • DryIoc(Prism 8+ 推荐默认)
  • Autofac
  • Microsoft.Extensions.DependencyInjection(.NET 原生 DI)

📌 从 Prism 8 开始,默认容器是 DryIoc(轻量、快速、功能强)。


🔧 在 Prism 中,"谁来 new"?

答案是:Prism 的 DI 容器(如 DryIoc) + Prism 框架本身

具体流程如下:

1️⃣ 你在 App.xaml.cs 中注册服务
复制代码
public partial class App : PrismApplication
{
    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // 注册你的服务
        containerRegistry.Register<IEmailService, SmtpEmailService>();
        containerRegistry.RegisterForNavigation<MainView, MainViewModel>(); // 注册页面和 VM
    }

    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>(); // ← 这里是第一个 "new" 的起点!
    }
}

RegisterTypes 就是你的 Composition Root(组合根) ------ 所有依赖关系在这里声明。


2️⃣ Prism 自动创建 ViewModel 并注入依赖

假设你的 MainViewModel 长这样:

复制代码
public class MainViewModel : BindableBase
{
    private readonly IEmailService _emailService;

    public MainViewModel(IEmailService emailService) // ← 构造函数注入
    {
        _emailService = emailService;
    }

    public void OnSendEmail()
    {
        _emailService.Send("Hello from Prism!");
    }
}

当你导航到 MainView 时:

  1. Prism 发现 MainView 关联了 MainViewModel

  2. Prism 调用 DI 容器container.Resolve<MainViewModel>()

  3. 容器发现 MainViewModel 需要 IEmailService

  4. 容器根据注册规则,执行:

    复制代码
    var emailService = new SmtpEmailService();     // ← 容器在 new!
    var viewModel = new MainViewModel(emailService); // ← 容器在 new!
  5. Prism 把 viewModel 自动赋值给 MainView.DataContext

💡 你全程没有写任何 new,Prism + DI 容器全包了!


🧩 Prism 中的关键角色

组件 职责
IContainerRegistry 你在 RegisterTypes 中使用的注册接口
IContainerProvider / Container 提供 Resolve<T>() 方法(慎用!)
ViewModelLocator(自动) Prism 默认启用,自动将 View 和 ViewModel 关联并注入
Navigation Service 导航时自动从容器解析目标 ViewModel

⚠️ 注意事项 & 最佳实践

✅ 1. 不要手动 new ViewModel

❌ 错误:

复制代码
// 不要这样做!依赖无法注入
DataContext = new MainViewModel();

✅ 正确:

  • 使用 RegisterForNavigation<MainView, MainViewModel>()
  • 或让 Prism 自动按命名约定匹配(MainViewMainViewModel

✅ 2. 避免在 ViewModel 中调用 Container.Resolve()

❌ 反模式(服务定位器):

复制代码
public void SomeMethod()
{
    var service = Container.Resolve<IMyService>(); // 隐藏依赖!
}

✅ 正确:始终通过构造函数注入

复制代码
public MainViewModel(IMyService myService) // 显式声明依赖
{
    _myService = myService;
}

✅ 3. 理解生命周期

Prism 默认注册方式:

  • Register<T>Transient(每次解析都新建)
  • RegisterSingleton<T>Singleton(全局唯一)

对于 ViewModel,Prism 每次导航都会创建新实例(除非你自定义导航逻辑)。


✅ 4. 如何注入到非 ViewModel 类?

比如一个 OrderProcessor 服务:

复制代码
// 注册
containerRegistry.Register<IOrderProcessor, OrderProcessor>();

// 在 ViewModel 中使用
public class MainViewModel
{
    public MainViewModel(IOrderProcessor processor) { ... }
}

只要它被容器管理的对象依赖,就会自动注入。


🛠️ 如果你想查看"谁在 new"

你可以替换为 .NET 原生 DI 容器(更透明):

复制代码
// 安装 Prism.Microsoft.DependencyInjection
protected override IContainerExtension CreateContainerExtension()
{
    return PrismContainerExtension.Create(new ServiceCollection());
}

然后你在 RegisterTypes 中其实就是在配置 IServiceCollection,和 ASP.NET Core 一样!


✅ 总结:Prism 中的 "new" 在哪?

场景 谁在 new 你是否需要关心
服务(IEmailService) DI 容器(DryIoc/.NET DI) ❌ 不需要
ViewModel Prism + DI 容器(通过导航触发) ❌ 不需要
View(Window/Page) WPF/MAUI 框架(但 DataContext 由 Prism 设置) ❌ 不需要
App 启动时的 Shell 你在 CreateShell() 中调用 Container.Resolve<MainWindow>() ✅ 这是你唯一接触"解析"的地方

🧠 记住
Prism 是你的"DI 管家" ------ 它不仅帮你注册、解析、注入,还把 MVVM、导航、模块化全串起来了。

你只需专注业务逻辑,new 的烦恼交给 Prism!

相关推荐
马达加斯加D1 小时前
C# --- 如何写UT
前端·c#·log4j
ComplexPy1 小时前
ZKMall-B2B2C Redission延时队列
java·redis
q***96581 小时前
深入解析Spring Boot中的@ConfigurationProperties注解
java·spring boot·后端
java1234_小锋1 小时前
讲讲Mybatis的一级、二级缓存?
java·开发语言·mybatis
e***87701 小时前
记录 idea 启动 tomcat 控制台输出乱码问题解决
java·tomcat·intellij-idea
发现你走远了1 小时前
2025 idea 指定配置环境运行springboot 设置active和env启动端口,多端口启动 (保姆级图文)
java·spring boot·intellij-idea
sanggou1 小时前
Java秒杀系统设计与实现
java
eggcode2 小时前
C#开源库ACadSharp将Dwg转Dxf
c#·dxf·dwg
情怀姑娘2 小时前
面试题---------------场景+算法
java·算法·mybatis