C#摸鱼实录——IoC与DI案例详解
IoC(控制反转)与DI(依赖注入)
开一个新的模块哈,在这个模块里面,我们主要讲一个东西如何使用,尽量不纠结概念,简单过过
之前老是被人说,是不是过于偏向于学院派了,所以从现在开始,我们将只关注能不能用
这个模块里面,我想讲的,大多数是在实际项目中常用的东西,例如一些NuGet 包,一个语法,或者某种设计模式
不过不过多描述概念了,不讲官方那些罗里吧嗦的概念,只需要理解他是什么鬼东西,干什么的,怎么用即可
大抵就是学院派和江湖派的区别吧
顺便后面我要是忘记这个东西怎么用了,还可以回来看看文档,顺便,这就是我未来AI的蒸馏对象 我蒸馏我自己
然后,为什么要进行这么古老的学习方式,废话,这年头AI快把初级员工的路堵死了,
不来点古法编程,抽象能力提升很慢的,用了AI几个月,发现初级迈向中级,你不古法编程就等死吧
而且上班摸鱼时间一大把,系统性的学习学习怎么了,打发时间也挺好的,然后深入学习一下IoC的思想
废话少说,进入正题
一.DI依赖注入 — 概念
[!TIP]
如果你不想看文字,或者觉得我这一块讲的不是特别明白的,想看视频教学的话
推荐一位up做的关于依赖注入的教学视频,大概30分钟左右的教学,
只不过后面几个视频初学者容易看不懂
【.Net-依赖注入】从依赖说起_哔哩哔哩_bilibili
很多人可能经常使用依赖注入,但是不知道他叫什么,DI是什么鬼东西,其实看一眼代码就了解了 不懂你就再看一眼
1.什么是依赖(Dependency)?
-
一个对象要工作,需要另一个对象的帮助,没有另一个对象就完成不了
-
/// 因为产品需要零件A,所以产品依赖于零件A /// 即:零件A就是产品的依赖 public class 零件A { public int GetID() => 100; } public class 产品 { private readonly 零件A _a; public 产品(零件A a) => _a = a; }
2.什么是注入(Injection)?
-
把对象交给另一个对象使用
-
// 通过对象product使用了对象a var a = new 零件A(); var product = new 产品(a);
-
3.什么是依赖注入(Dependency Injection)?
-
依赖注入 = 依赖 + 注入
- 即:对象所需要的依赖由外部提供,而不是自己创建
-
下面是依赖注入的一点基本概念,结合上面的内容,已经写的非常清楚了,就不再过多阐述
-
// 一个用于示例的空类DbService public interface IDbService { void Insert(); } public class DbService : IDbService { public DbService() { } public void Insert() => Console.WriteLine("====================================="); } /// <summary> /// 传统写法 - 不使用依赖注入(模块之间强依赖,耦合度高) /// </summary> public class NO_DI { // 🌱钱没给够,你自己new吧 private DbService _db = new DbService(); public void Save() => _db.Insert(); } /// <summary> /// 使用依赖注入(松散解耦) /// </summary> public class Yes_DI { private readonly DbService _db; // 🌱钱给够了,直接从外部“注入” public Yes_DI(DbService db) => _db = db; public void Save() => _db.Insert(); } /// <summary> /// 依赖注入常用三种方式(但是基本上还是以构造注入为主) /// </summary> /// <summary> /// 1.1构造注入 /// </summary> public class 构造注入 { private readonly DbService _db; public 构造注入(DbService db) => _db = db; // var a = new A(); // var demo = new 构造注入(a); } /// <summary> /// 1.2.属性注入 /// </summary> public class 属性注入 { public 属性注入() { } public DbService DB { get; set; } = null!; // 属性注入 demo = new 属性注入(); // demo.DB = new DbService(); } /// <summary> /// 1.3.方法注入 /// </summary> public class 方法注入 { public 方法注入() { } public void Execute(DbService db) { } // 方法注入.Execute(new DbService()); }
二.IoC(控制反转)— 概念
1.什么是控制(Control)?
-
谁决定对象如何产生和使用
- 控制权:决定某件事情如何进行的权力
- 在IoC中,特指:创建什么对象,什么时候创建,对象如何创建对象的决定权
- 看不懂就看下面的例子,一眼秒懂
- 控制权:决定某件事情如何进行的权力
-
// 产品控制着零件A的创建 // 即:产品拥有创建零件A的控制权 // 缺点,产品和零件A已经绑死了,高度耦合,扩展等死,后人挠头,直骂屎山 public class 零件A { } public class 产品 { private readonly 零件A _a; public 产品() { _a = new 零件A(); } }
2.什么是反转(Inversion)?
-
反转 = 原来的方向反过来了
- 原本由A负责的事情,改由B负责
-
在IoC中通常指:控制权发生变化,由内部控制变成外部控制
-
public class 零件A { } //======================================= // 内部控制 public class 产品 { private readonly 零件A _a; public 产品() { _a = new 零件A(); } } //======================================= // 外部控制 public class 产品 { private readonly 零件A _a; public 产品(零件A a) { _a = a; } }
3.什么是控制反转(Inversion of Control)?
-
控制方向被反过来了,所以叫控制反转 -
控制反转:原本由对象自己掌握的控制权,转移给了外部对象或容器
- 但是需要注意的是,IoC是一种思想,它并不是某种具体的实现
- 换句话说,DI是IoC的一种实现方式,依赖注入就是使用控制反转的思想
- 即:依赖注入(DI)是实现IoC最常见的方式之一
-
# 原来:产品内部控制零件A 产品 ↓ # 控制 零件A #================================================== # 现在:外部同时控制零件和产品,然后把零件A交给产品使用 外部 ↓ # 控制 零件A 外部 ↓ # 控制 产品
三.DI容器 —— 具体使用示例
-
在此之前我们先添加个控制反转(IoC)的NuGet包,这个是官方给的实现控制反转的容器
-
你可以选择在
NuGet包管理器 → 管理解决方案的NuGet程序包中搜索Microsoft.Extensions.DependencyInjection下载 -
只不过我还是喜欢命令行操作下载,这很酷,并且非常快捷,难道不是吗
-
# 扩展 -> NuGet包管理器 -> 程序包管理控制台 -> 输入下面的命令行 dotnet add package Microsoft.Extensions.DependencyInjection
-
-
然后,再添加几个一会示例中要使用的类,为了更好的理解,这里我使用中文来更好的演示
-
/// 这里我们使用 Nuget顶级包Microsoft.Extensions.DependencyInjection(官方DI容器) 来实现IOC操作 /// <summary> /// 零件接口 /// </summary> public interface 零件接口 { int GetID(); } /// <summary> /// 零件A /// </summary> public class 零件A : 零件接口 { public int ID { get; set; } = 1001; public int GetID() => ID; } /// <summary> /// 零件B /// </summary> public class 零件B : 零件接口 { public int ID { get; set; } = 2002; public int GetID() => ID; } /// <summary> /// 产品 /// </summary> public class 产品 { private readonly 零件接口 _part; public 产品(零件接口 part) { _part = part; } public void ShowInfo() { Console.WriteLine($"产品使用的零件ID:{_part.GetID()}"); } }
-
DI容器使用的4个基本步骤
-
使用容器的四个步骤基本上就是:
创建 -> 注册 -> 构建 -> 调用-
# DI容器使用的4个步骤 1. 创建容器生成器 # ServiceCollection 2. 注册服务 # AddXXX 3. 构建容器 # BuildServiceProvider 4. 获取服务 # GetService
-
-
这里直接上代码,后面依次解说
-
/// 不使用IoC容器 零件A part = new 零件A(); 产品 产品1 = new 产品(part); 产品1.ShowInfo(); //================================================================= /// 使用IoC容器:创建 -> 注册 -> 构建 -> 调用 /* 一.创建容器生成类(服务集合) */ ServiceCollection containerBuilder = new ServiceCollection(); /* 二.注册容器中的服务信息(注册实例) —— 服务类型,实现类型,生命周期 */ containerBuilder.AddSingleton<产品>(); containerBuilder.AddTransient<零件接口, 零件A>(); /* 三.生成容器 */ IServiceProvider container = containerBuilder.BuildServiceProvider(); containerBuilder.MakeReadOnly(); // 将生成类集合声明为只读,后续再添加(Add)服务会进行报错 // containerBuilder.AddSingleton<产品>(); // => 使用MakeReadOnly后报错 /* 四.服务调用(从容器获取对象) */ 产品? product = container.GetService<产品>(); 产品 product2 = container.GetRequiredService<产品>();
-
1.创建容器生成器(ServiceCollection)
- 创建一个服务集合,用于保存所有服务注册信息
这没什么好讲的,你完全不需要关心这个是什么,反正是创建服务集合就对了
/* 一.创建容器生成类(服务集合) */
ServiceCollection containerBuilder = new ServiceCollection();
2.注册服务(AddXXX)
- 本质上是告诉容器,使用什么服务类型,什么实现类型,生命周期是什么
- 需要什么服务
- 创建什么对象
- 使用什么生命周期
2.1 三种注册服务:单例,瞬时,作用域
/* 二.注册容器中的服务信息(注册实例) —— 服务类型,实现类型,生命周期 */
containerBuilder.AddSingleton<产品>();
containerBuilder.AddTransient<零件接口, 零件A>();
// 三种注册服务
// 1.AddSingleton(单例):只创建一个单例
// 2.AddTransient(瞬时):每次获取都创建一个新对象(每次调用都是一个新的对象)
// 3.AddScoped(作用域):多用于Web服务(同一个会话,是同一个服务),WPF中很少使用
// 1)单例
// 服务对象 = 实现对象
containerBuilder.AddSingleton<产品>();
// 2)瞬时
// (1)AddTransient<服务类型>() 等价于 AddTransient<服务类型, 实现类型>(),但是服务类型 = 实现类型
// (2)AddTransient<服务类型, 实现类型>() => 服务类型通常为接口或者基类
containerBuilder.AddTransient<零件A>();
containerBuilder.AddTransient<零件接口, 零件A>();
// 3)作用域(不会Web我就偷懒不讲了哈)
// AddScoped<接口或父类, 实现类>()
// 尝试注册 - 用法和上面一样,效果 -> 如果没注册就注册,注册了就通过
// services.AddSingleton();
// services.AddScoped();
// services.AddTransient();
2.2 三种常用注册方式
-
这里只讲述三种最常见的注册方式:类型注册,实例注册和工厂注册
-
注册方式 示例 容器负责创建? 常用程度 类型注册 AddXXX<服务类型>()
(服务类型 = 实现类型)✅ ⭐⭐⭐⭐⭐ AddXXX<服务类型, 实现类型>()✅ ⭐⭐⭐⭐ 实例注册 AddXXX(obj)❌ ⭐⭐⭐ 工厂注册 AddXXX(sp => ...)部分负责 ⭐⭐
-
0)三种注册方式详细总结
| 对比项 | 类型注册 | 实例注册 | 工厂注册 |
|---|---|---|---|
| 典型语法 | AddXXX<T>()AddXXX<TService, TImplementation>() |
AddXXX(obj) |
AddXXX(sp => ...) |
| 注册内容 | 类型信息 | 已存在的对象实例 | 对象创建方法(Lambda) |
| 对象何时创建 | 获取服务时由容器创建 | 注册前已创建 | 获取服务时执行工厂函数创建 |
| 谁决定如何创建对象 | 容器 | 屏幕前苦逼的你 | 屏幕前苦逼的你 |
| 容器是否负责创建 | ✅ | ❌ | 部分负责 |
| 容器是否管理生命周期 | ✅ | ✅ | ✅ |
| 是否支持依赖注入 | ✅ | ❌(对象已创建) | ✅ |
| 是否可以获取容器中的其它服务 | 自动注入 | ❌ | ✅(通过 sp.GetRequiredService()) |
| 适用场景 | 日常开发最常用 | 已存在对象、第三方对象 | 复杂初始化、特殊参数、条件创建 |
| 常用程度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| WPF使用频率 | 极高 | 偶尔 | 偶尔 |
1)类型注册:容器负责创建
-
类型注册有两种:
-
1.服务类型 = 注册类型
-
// 服务类型 = 实现类型 AddXXX<服务类型>()-
这是WPF中最常用的注册方式
-
services.AddSingleton<产品>(); services.AddTransient<零件A>(); // 等价于 services.AddSingleton<产品, 产品>(); services.AddTransient<零件A, 零件A>();
-
-
2.服务类型 ≠ 注册类型
-
AddXXX<服务类型, 实现类型>()-
有博客也叫这种方式为接口 / 基类注册
-
// 接口注册 services.AddTransient<ILogger, FileLogger>(); // 基类注册 services.AddSingleton<零件Base, 零件A>();
-
-
2)实例注册:你先创建,再交给容器
-
实例注册主要针对于已经存在的对象,直接使用存在的的实例
-
❌
容器不能使用已经存在的实例 -
✔容器不会创建已经存在的实例(实例已经创建完成,因此容器不再负责创建)
-
# 类型注册:使用容器注册产品 容器 ↓ new 产品() #======================================== # 实例注册:将产品实例交给容器 new 产品() ↓ 容器
-
-
代码示例:
-
SerialPort port = new SerialPort("COM1", 115200); services.AddSingleton(port);
3)工厂注册:你告诉容器以后怎么创建
-
工厂注册,实际上就是告诉容器,不要使用默认的方法,使用我提供给你的方法
-
所以有其他博客说,工厂注册的使用时机:需要 复杂初始化,需要配置参数,根据条件创建对象时
-
[!CAUTION]
- 注册工厂时并不会立即执行 Lambda 表达式
- Lambda 只是被容器保存起来,
- 当获取服务(调用服务)时,容器才会执行该 Lambda 来创建对象
- 注册工厂时并不会立即执行 Lambda 表达式
-
-
containerBuilder.AddSingleton<零件A>(); /// 这里是sp指IServiceProvider(当前容器) containerBuilder.AddSingleton<产品>(sp => { // 现在这个大括号里面的内容就是你自定义的对象创建过程 var part = sp.GetRequiredService<零件A>(); return new 产品(part); }); -
获取产品 ↓ 执行工厂函数 ↓ 从容器获取零件A ↓ 创建并且返回产品
3.构建容器(BuildServiceProvider)
-
这个也不需要多讲,但是有一个特别需要注意的地方
-
[!IMPORTANT]
-
BuildServiceProvider()会根据当前注册信息生成真正的 DI 容器 -
换句话说,前面的创建集合,还并没有生成一个真正的DI容器
-
在调用之前:只有注册信息
-
调用之后:才能获取服务
-
-
/* 三.生成容器 */
IServiceProvider container = containerBuilder.BuildServiceProvider();
containerBuilder.MakeReadOnly(); // 将生成类集合声明为只读,后续再添加服务会进行报错
// containerBuilder.AddSingleton<产品>(); //=> 使用MakeReadOnly后报错
4.获取服务(GetService)
- 一般情况下,我们会获取不带键的服务(如下),但是现在官方给了我们一种比较新的写法(带键)
/* 四.服务调用(从容器获取对象) */
// 不带键
产品? product = container.GetService<产品>();
产品 product2 = container.GetRequiredService<产品>();
-
为什么需要带键?同一个服务类型,可能对应多个实现对象,我想指定实现对象,怎么办?
-
services.AddSingleton<IProtocol, ModbusProtocol>(); services.AddSingleton<IProtocol, OpcUaProtocol>(); // ...... // 两个实现对象,容器该怎么选择呢?会选择最后一个注册的实现 // 如果我想要选择中间的或者第一个怎么办呢? // 所以出现了带键的服务,注册时给它一个标签,获取时,指定对应标签,那就完事 var protocol = provider.GetRequiredService<IProtocol>();
-
4.1 不带键的服务
1)GetService
-
Get-获取,Service-服务,GetService-获取服务,但是你没说是不是必须的- 有就给,没有就算了(返回 null)
-
# 如果产品存在 => 返回对象 # 如果产品不存在 => 返回 null 产品? product = container.GetService<产品>();
2)GetRequiredService
-
Required-必须的,今天你要是不把服务交出来,这个程序就别想跑了!- 必须要有,没有就直接报错
-
# 如果产品存在 => 返回对象 # 如果产品不存在 => 返回 直接抛异常 产品 product = container.GetRequiredService<产品>();
4.2 带键的服务
说白了,和不带键的服务用法基本上一致,就是在注册时给它加了个标签
1)GetKeyedService
-
/// 注册 containerBuilder.AddKeyedSingleton<产品>("产品"); // ....... /// 获取服务 // 获取产品 产品 product = container.GetRequiredKeyedService<产品>("产品");
2)GetRequiredKeyedService
-
/// 注册 containerBuilder.AddKeyedSingleton<零件接口, 零件A>("零件A"); // ....... /// 获取服务 // 获取零件 零件接口 part = container.GetRequiredKeyedService<零件接口>("零件A");
四.流程图
-
感谢ChatGPT为我们生成了一张总的流程图(其实是我懒,不想画)
-
IoC(思想) ↓ DI(实现方式) ↓ DI容器 # ================================ 1.创建容器 ↓ ServiceCollection 2.注册服务 ↓ AddSingleton AddTransient AddScoped 3.构建容器 ↓ BuildServiceProvider 4.获取服务 ↓ GetRequiredService GetService
五.实际案例
1.回顾
但是在此之前,我们先回顾一下内容,不然——
你不会以为就这么完了吗,我有说过结束了吗,那和我以前的博客有什么区别?
注册(AddXXX)
↓
向容器登记对象创建规则
调用(GetXXX)
↓
让容器按照规则创建并返回对象
//========================================================
// 参考前面章节使用的代码(三.DI容器中的几个类)
// 去掉中间的步骤,单独使用这两组代码(注册+调用),会出现什么问题?
/* 第一组 */
services.AddSingleton<产品>();
产品 product = container.GetRequiredService<产品>();
/* 第二组 */
services.AddTransient<零件接口, 零件A>();
零件接口? part = container.GetService<零件接口>();
// 问:为什么这里的服务调用是 获取"零件接口"的对象 ,为什么不可以是使用"零件"的对象?
// 别瞎思考了,这里你看起来像对象的东西,实际上只是一个服务标识,即 注册为零件接口服务 的标识
1)注册到底注册了个什么东西出来?
-
注册阶段并不会创建对象,而是在告诉容器执行什么样的规则(以这里的两行代码为例):
-
以后如果有人需要产品,就创建产品对象
-
以后如果有人需要零件接口,就创建零件A对象
-
# 注册阶段 服务类型: 产品 零件接口 ↓ ↓ 实现类型: 产品类 零件A类 ######################################## # 类似于容器表 产品 零件接口 ↓ ↓ 产品类 零件A类 # 相当于(DI容器)说明书 我要什么服务 ↓ 应该创建什么对象 # 即:需要(服务类型)时,创建(实现类型)对象
-
-
2)当我们调用服务时,又干了什么事情?
-
你可能以为的:
-
# 产品 product = container.GetRequiredService<产品>(); 我要产品 => 查找登记表 => 找到产品类 => 查看产品构造函数 ↓ 发现产品需要零件接口 => 查找登记表 => 零件接口 -> 零件A ↓ 创建零件A ↓ 创建产品 ↓ 返回产品 # 零件接口? part = container.GetService<零件接口>(); 我要零件接口 => 查找登记表 => 零件接口 -> 零件A ↓ 创建零件A ↓ 返回零件A ↓ 调用GetID()
-
-
但实际上我在上面埋了个雷
-
services.AddSingleton<产品>(); 产品 product = container.GetRequiredService<产品>(); # 注意,这里只有一个单例注册,并没有services.AddTransient<零件接口, 零件A>(); 这个时候你获取服务会发生下面事件: 我要产品 -> 查找登记表 -> 找到产品类 -> 查看产品构造函数 ↓ 发现产品需要零件接口 -> 查找登记表 ↓ 未找到:零件接口 -> 某个实现 ↓ 无法创建零件接口对应对象 ↓ 无法创建产品 ↓ 报错 # 恭喜你,现在喜提编译器警告一次 System.InvalidOperationException:“Unable to resolve service for type 'DI_Demo.零件接口' while attempting to activate 'DI_Demo.产品'.”
-
-
为什么会报错?初学者可能会产生一个误解:
- 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?
- 问题是接口不是对象,他不能实例化
- 接口有实现吗,他有对象吗,它没对象啊,纯单身狗一条啊
- 接口只是一种约定,或者说草案,不是一个对象啊
- 所以会出现:产品需要零件接口,但是接口不能创建对象,然后就崩了
# 换句话说,你要是可以帮我实现下面这行代码算你厉害 ❌零件接口 part = new 零件接口(); # 容器真正要知道的是下面内容: 零件接口 ↓ 到底应该创建谁? ↓ 零件A,B,C中的哪一个? # 所以必须注册:明确知道是注册的谁 services.AddTransient<零件接口, 零件A>();
- 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?
-
但是,这个时候你可能还会非常疑惑:
-
那单例注册到底有什么用啊?为什么只放一个单例注册+一个单例注册的服务调用基本上无法使用?
-
单例注册往往不是用来解决依赖的,仅说明,该对象只创建一次
-
因为一个服务很少是孤立存在的,也因此单例注册几乎很少单独使用
-
一个实际案例
-
/// <summary> /// 配置服务(整个程序只有一份,所以需要被注册为单例服务) /// </summary> public class ConfigService { public string ComPort { get; } = "COM3"; public int BaudRate { get; } = 115200; } /// <summary> /// XXX通讯模块 /// </summary> public class XXXService { private readonly ConfigService _config; public XXXService(ConfigService config) { _config = config; } public void Connect() { Console.WriteLine($"连接XXX:{_config.ComPort},波特率:{_config.BaudRate}"); } } // 1.创建服务集合 ServiceCollection services = new(); // 2.注册服务 services.AddSingleton<ConfigService>(); services.AddTransient<XXXService>(); // 3.构建容器 IServiceProvider provider = services.BuildServiceProvider(); // 4.获取服务 XXXService xxx = provider.GetRequiredService<XXXService>(); // 5.使用服务 xxx.Connect();
-
-
2.理解误区
1)误区1:依赖是在注册时就创建的
-
有一点必须记住:
- 解决依赖的从来都不是什么
Transient、Singleton、Scoped, - 而是DI容器根据注册表找到对应实现并创建对象
- 那三个注册方式只能决定创建出来的对象能活多久
- 解决依赖的从来都不是什么
-
实际上,依赖这种东西在类中构造函数定义和实现的时候就已经定义好了(依赖关系已定义,这里以构造依赖为例)
-
依赖关系在编写构造函数时就已经确定好了
-
DI容器只是负责解析并创建这些依赖对象
-
# 很多很多教程都喜欢这样写: services.AddSingleton<产品>(); services.AddTransient<零件接口, 零件A>(); # 会让很多人误解: # Singleton负责产品,Transient负责依赖 # 但是假设我这样写,阁下如何应对呢 services.AddSingleton<产品>(); services.AddSingleton<零件接口, 零件A>();
-
2)误区2:认为单例注册是无法获取服务的
-
很多实际业务服务不会单独存在,但单例注册本身完全可以单独获取
-
而且当一堆单例注册出现的时候就不一样了
果然,还是人多力量大-
多个服务注册在一起后,容器可以在获取服务时自动解析依赖关系(不仅局限于单例注册)
-
public class X_Service { public X_Service(ConfigService config, LogService log) { } } services.AddSingleton<ConfigService>(); services.AddSingleton<LogService>(); services.AddSingleton<X_Service>(); X_Service x = provider.GetRequiredService<X_Service>(); -
我要 XXXService ↓ 发现需要 ConfigService ↓ 获取 ConfigService ########################### 发现需要 LogService ↓ 获取 LogService ########################### 创建 XXXService ↓ 返回 XXXService
-
3)误区3:认为使用注册必须一一对应
-
拜托,这是C#,不是C++,初学者甚至都可以看明白很多官方库代码,不要想得过于复杂
-
如果不知道怎么注册,你就将注册看做一个列表,你只需要将你可以想到的依赖关系和单例情况全部写上去就完事了
-
容器可以在获取服务时自动解析依赖关系,你要使用对应的服务对象,他自己找到怎么自动查找和解析
-
注册阶段可以把已知的服务和依赖关系全部登记到容器中
-
而且只要不是实例注册,绝大多数服务都不会立即创建,
-
即使暂时没有使用,也基本不会产生明显开销
-
public class DatabaseService { public void Save(string msg) { Console.WriteLine($"保存数据:{msg}"); } } public class LogService { public void Write(string msg) { Console.WriteLine($"记录日志:{msg}"); } } public class OrderService { private readonly DatabaseService _db; private readonly LogService _log; public OrderService(DatabaseService db, LogService log) { _db = db; _log = log; } public void CreateOrder() { _db.Save("订单"); _log.Write("创建订单成功"); } } public class OrderController { private readonly OrderService _orderService; public OrderController(OrderService orderService) { _orderService = orderService; } public void Create() => _orderService.CreateOrder(); } // 1.创建 ServiceCollection services = new(); // 2.注册 services.AddSingleton<DatabaseService>(); services.AddSingleton<LogService>(); services.AddSingleton<OrderService>(); services.AddSingleton<OrderController>(); // 4.构建 IServiceProvider provider = services.BuildServiceProvider(); // 5.获取服务 OrderController controller = provider.GetRequiredService<OrderController>(); // 6.使用 controller.Create();
-
-
4)误区4:注册时,若服务对象 ≠ 实现对象,获取服务时返回对象总认为是服务对象
-
services.AddTransient<零件接口, 零件A>(); 零件接口? part = container.GetService<零件接口>(); -
初学时,你以为的,创建了一个零件接口服务,然后返回了一个零件接口服务
-
不不不,这得看你实际代码,例如这里实际上返回的是零件A,只不过使用的服务是接口
-
// 让ChatGPT蒸馏了这篇博客,然后写了一个Demo using Microsoft.Extensions.DependencyInjection; #region 服务定义 /// <summary> /// 零件接口 /// </summary> public interface IPart { Guid Id { get; } } /// <summary> /// 零件A /// </summary> public class PartA : IPart { public Guid Id { get; } = Guid.NewGuid(); } #endregion internal class Program { static void Main(string[] args) { /* 一.创建服务集合 */ ServiceCollection services = new(); /* 二.注册服务 */ services.AddTransient<IPart, PartA>(); /* 三.构建容器 */ IServiceProvider provider = services.BuildServiceProvider(); /* 四.获取服务 */ IPart part1 = provider.GetRequiredService<IPart>(); IPart part2 = provider.GetRequiredService<IPart>(); IPart part3 = provider.GetRequiredService<IPart>(); Console.WriteLine($"part1 : {part1.Id}"); Console.WriteLine($"part2 : {part2.Id}"); Console.WriteLine($"part3 : {part3.Id}"); Console.WriteLine(); Console.WriteLine( $"part1 == part2 : {ReferenceEquals(part1, part2)}"); Console.WriteLine( $"part2 == part3 : {ReferenceEquals(part2, part3)}"); } }
3.回过头来看另一个问题:产品运行期间需要动态切换实现零件A和B怎么办?
-
这里给了3种不同的方案,详细的我就不过多解释了,主要是因为都14点了,该摸鱼听听音乐了
-
使用DI容器的方案是修改方案3,方案1远古方案,方案2是设计模式
-
原代码:
-
/// 这里我们使用 Nuget顶级包Microsoft.Extensions.DependencyInjection(官方DI容器) 来实现IOC操作 /// <summary> /// 零件接口 /// </summary> public interface 零件接口 { int GetID(); } /// <summary> /// 零件A /// </summary> public class 零件A : 零件接口 { public int ID { get; set; } = 1001; public int GetID() => ID; } /// <summary> /// 零件B /// </summary> public class 零件B : 零件接口 { public int ID { get; set; } = 2002; public int GetID() => ID; } /// <summary> /// 产品 /// </summary> public class 产品 { private readonly 零件接口 _part; public 产品(零件接口 part) { _part = part; } public void ShowInfo() { Console.WriteLine($"产品使用的零件ID:{_part.GetID()}"); } }
-
1)修改方案1:不使用DI容器
-
public interface IPart { int GetID(); } public class PartA : IPart { public int GetID() => 1001; } public class PartB : IPart { public int GetID() => 2002; } public class Product { /// 当前使用的零件 private IPart _part; /// 创建产品时指定初始零件 public Product(IPart part) { _part = part; } /// 动态更换零件 public void ChangePart(IPart part) { _part = part; } /// 显示产品信息 public void ShowInfo() { Console.WriteLine( $"当前零件ID:{_part.GetID()}"); } } internal class Program { static void Main() { /* 创建产品并使用零件A */ Product product = new Product(new PartA()); product.ShowInfo(); Console.WriteLine(); /* 更换为零件B */ product.ChangePart(new PartB()); product.ShowInfo(); } }
2)修改方案2:工厂模式
-
/// <summary> /// 零件接口 /// </summary> public interface IPart { int GetID(); } /// <summary> /// 零件A /// </summary> public class PartA : IPart { public int GetID() => 1001; } /// <summary> /// 零件B /// </summary> public class PartB : IPart { public int GetID() => 2002; } /// <summary> /// 零件工厂接口 /// </summary> public interface IPartFactory { IPart Create(string partType); } /// <summary> /// 零件工厂 /// </summary> public class PartFactory : IPartFactory { public IPart Create(string partType) { return partType switch { "A" => new PartA(), "B" => new PartB(), _ => throw new ArgumentException("未知零件类型") }; } } /// <summary> /// 产品 /// </summary> public class Product { private readonly IPartFactory _factory; private IPart? _part; /// <summary> /// 产品依赖工厂 /// </summary> public Product(IPartFactory factory) { _factory = factory; } /// <summary> /// 根据类型切换零件 /// </summary> public void ChangePart(string partType) { _part = _factory.Create(partType); } public void ShowInfo() { Console.WriteLine($"当前零件ID:{_part?.GetID()}"); } } internal class Program { static void Main() { Product product = new Product(new PartFactory()); product.ChangePart("A"); product.ShowInfo(); Console.WriteLine(); product.ChangePart("B"); product.ShowInfo(); } }
3)修改方案3:保留DI容器,使用键
-
using Microsoft.Extensions.DependencyInjection; public interface IPart { int GetID(); } public class PartA : IPart { public int GetID() => 1001; } public class PartB : IPart { public int GetID() => 2002; } public class Product { private IPart? _part; /// <summary> /// 更换零件 /// </summary> public void ChangePart(IPart part) => _part = part; public void ShowInfo() => Console.WriteLine($"当前零件ID:{_part?.GetID()}"); } internal class Program { static void Main() { /* 一.创建服务集合 */ ServiceCollection services = new(); /* 二.注册服务 */ // 注册带键服务 services.AddKeyedTransient<IPart, PartA>("A"); services.AddKeyedTransient<IPart, PartB>("B"); // 注册产品 services.AddSingleton<Product>(); /* 三.构建容器 */ IServiceProvider provider = services.BuildServiceProvider(); /* 四.获取产品 */ Product product = provider.GetRequiredService<Product>(); /* 使用零件A */ IPart partA = provider.GetRequiredKeyedService<IPart>("A"); product.ChangePart(partA); product.ShowInfo(); Console.WriteLine(); /* 使用零件B */ IPart partB = provider.GetRequiredKeyedService<IPart>("B"); product.ChangePart(partB); product.ShowInfo(); } }
终于又到了一篇一次的吐槽时刻了,突然觉得最近上班好无聊啊,但是只要我一开始写点东西
现场电话就打过来了,只要我开始摸鱼,真的什么事情也没有,当然,也绝对绝对不是我特别喜欢摸鱼
这几个月倒是用AI写了很多东西,但是写到后面越发发现,突然让我沉下心来敲几行代码
也是愈发手生了,抽象能力也基本上没提高到多少,
就像这篇博客一样,可能你需要花费好几天,甚至一两周的时间
才可以完全消化完毕,并且写出来,但交给AI真的就只是一两分钟的事情
你一边感叹于初级员工真的一点用处都没有,只要一个企业想,随时随地可以被优化
当你想要尝试突破初级,来到中级的这个层次,你突然会发现,还是得一步一个脚印
古法编程可能会被淘汰,但是你不能不会,你可以不是那种彻彻底底的古法编程
但是不使用AI写代码时,你也至少要有大致的思路,知道怎么用
说白了就是频繁的使用已经断绝了很多初级程序员的路,抽象能力上不去,
什么架构能力,问题排查能力,兜底能力,团队协作能力,全是无稽之谈
真的必须花费大量的时间在这写你所以为低级的知识上,你才会有所进步
当然,也有可能是因为我不是搞Java的,不会有担心框架和结束高速迭代淘汰产生的一些问题,
但是归根结底,提高抽象能力最直接的办法,也就是古法编程了......
哪怕你是去copy一部分代码,也比让智能体完完全全接手你的项目要好的多
然后再来谈谈业务,从大学到毕业工作到现在,这个词听到耳朵都快起茧了
到底什么是业务,我也仔细思考了一下,
我所认为的业务,就是当你拿到一个项目,不管是你熟悉的还是不熟悉的东西
哪怕你之前是干嵌入式的,别管什么内核,下位机,现在就是要你去写一个Web程序
你会怎么思考,给你一个强大的AI,你又会怎么设计
对于不同行业,不同领域的东西,你可以思考出怎么来做这个产品的能力
这就是我理解的业务能力
只要业务能力足够,很多人哪怕转行,也非常轻松
而不是像我现在这样,上班天天盯着电脑,你说做一个个人项目
自己都不知道自己的需求是什么,架构设计怎么做合适,我好像除了写点业务一无所有
但是当自己接到一个项目时,虽然架构不是特别熟悉,但是却好像明白我需要干什么
这依旧是值得思考的一个点,但是这些也只能后面慢慢来了

浙公网安备 33010602011771号