IoC(控制反转)与DI(依赖注入)
开一个新的模块哈,在这个模块里面,我们主要讲一个东西如何使用,尽量不纠结概念,简单过过
之前老是被人说,是不是过于偏向于学院派了,所以从现在开始,我们将只关注能不能用
这个模块里面,我想讲的,大多数是在实际项目中常用的东西,例如一些NuGet 包,一个语法,或者某种设计模式
不过不过多描述概念了,不讲官方那些罗里吧嗦的概念,只需要理解他是什么鬼东西,干什么的,怎么用即可
大抵就是学院派和江湖派的区别吧
顺便后面我要是忘记这个东西怎么用了,还可以回来看看文档,顺便,这就是我未来AI的蒸馏对象 我蒸馏我自己
然后,为什么要进行这么古老的学习方式,废话,这年头AI快把初级员工的路堵死了,
不来点古法编程,抽象能力提升很慢的,用了AI几个月,发现初级迈向中级,你不古法编程就等死吧
而且上班摸鱼时间一大把,系统性的学习学习怎么了,打发时间也挺好的,然后深入学习一下IoC的思想
废话少说,进入正题
一.DI依赖注入 --- 概念
!TIP
如果你不想看文字,或者觉得我这一块讲的不是特别明白的,想看视频教学的话
推荐一位up做的关于依赖注入的教学视频,大概30分钟左右的教学,
只不过后面几个视频初学者容易看不懂
很多人可能经常使用依赖注入,但是不知道他叫什么,DI是什么鬼东西,其实看一眼代码就了解了 不懂你就再看一眼
1.什么是依赖(Dependency)?
-
一个对象要工作,需要另一个对象的帮助,没有另一个对象就完成不了
c#
/// 因为产品需要零件A,所以产品依赖于零件A
/// 即:零件A就是产品的依赖
public class 零件A
{
public int GetID() => 100;
}
public class 产品
{
private readonly 零件A _a;
public 产品(零件A a) => _a = a;
}
2.什么是注入(Injection)?
-
把对象交给另一个对象使用
c#// 通过对象product使用了对象a var a = new 零件A(); var product = new 产品(a);
3.什么是依赖注入(Dependency Injection)?
-
依赖注入 = 依赖 + 注入
- 即:对象所需要的依赖由外部提供,而不是自己创建
-
下面是依赖注入的一点基本概念,结合上面的内容,已经写的非常清楚了,就不再过多阐述
c#
// 一个用于示例的空类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中,特指:创建什么对象,什么时候创建,对象如何创建对象的决定权
- 看不懂就看下面的例子,一眼秒懂
- 控制权:决定某件事情如何进行的权力
c#
// 产品控制着零件A的创建
// 即:产品拥有创建零件A的控制权
// 缺点,产品和零件A已经绑死了,高度耦合,扩展等死,后人挠头,直骂屎山
public class 零件A { }
public class 产品
{
private readonly 零件A _a;
public 产品()
{
_a = new 零件A();
}
}
2.什么是反转(Inversion)?
-
反转 = 原来的方向反过来了
- 原本由A负责的事情,改由B负责
-
在IoC中通常指:控制权发生变化,由内部控制变成外部控制
c#
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最常见的方式之一
shell
# 原来:产品内部控制零件A
产品
↓ # 控制
零件A
#==================================================
# 现在:外部同时控制零件和产品,然后把零件A交给产品使用
外部
↓ # 控制
零件A
外部
↓ # 控制
产品
三.DI容器 ------ 具体使用示例
-
在此之前我们先添加个控制反转(IoC)的NuGet包,这个是官方给的实现控制反转的容器
-
你可以选择在
NuGet包管理器 → 管理解决方案的NuGet程序包中搜索Microsoft.Extensions.DependencyInjection下载 -
只不过我还是喜欢命令行操作下载,这很酷,并且非常快捷,难道不是吗
shell# 扩展 -> NuGet包管理器 -> 程序包管理控制台 -> 输入下面的命令行 dotnet add package Microsoft.Extensions.DependencyInjection
-
-
然后,再添加几个一会示例中要使用的类,为了更好的理解,这里我使用中文来更好的演示
c#/// 这里我们使用 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个基本步骤
-
使用容器的四个步骤基本上就是:
创建 -> 注册 -> 构建 -> 调用shell# DI容器使用的4个步骤 1. 创建容器生成器 # ServiceCollection 2. 注册服务 # AddXXX 3. 构建容器 # BuildServiceProvider 4. 获取服务 # GetService -
这里直接上代码,后面依次解说
c#/// 不使用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)
- 创建一个服务集合,用于保存所有服务注册信息
这没什么好讲的,你完全不需要关心这个是什么,反正是创建服务集合就对了
c#
/* 一.创建容器生成类(服务集合) */
ServiceCollection containerBuilder = new ServiceCollection();
2.注册服务(AddXXX)
- 本质上是告诉容器,使用什么服务类型,什么实现类型,生命周期是什么
- 需要什么服务
- 创建什么对象
- 使用什么生命周期
2.1 三种注册服务:单例,瞬时,作用域
c#
/* 二.注册容器中的服务信息(注册实例) ------ 服务类型,实现类型,生命周期 */
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.服务类型 = 注册类型
c#// 服务类型 = 实现类型 AddXXX<服务类型>()-
这是WPF中最常用的注册方式
```c# services.AddSingleton<产品>(); services.AddTransient<零件A>(); // 等价于 services.AddSingleton<产品, 产品>(); services.AddTransient<零件A, 零件A>(); ```-
2.服务类型 ≠ 注册类型
c#AddXXX<服务类型, 实现类型>()-
有博客也叫这种方式为接口 / 基类注册
```c# // 接口注册 services.AddTransient<ILogger, FileLogger>(); // 基类注册 services.AddSingleton<零件Base, 零件A>(); ``` -
2)实例注册:你先创建,再交给容器
-
实例注册主要针对于已经存在的对象,直接使用存在的的实例
-
❌容器不能使用已经存在的实例
-
✔容器不会创建已经存在的实例(实例已经创建完成,因此容器不再负责创建)
shell# 类型注册:使用容器注册产品 容器 ↓ new 产品() #======================================== # 实例注册:将产品实例交给容器 new 产品() ↓ 容器
-
-
代码示例:
c#
SerialPort port = new SerialPort("COM1", 115200);
services.AddSingleton(port);
3)工厂注册:你告诉容器以后怎么创建
-
工厂注册,实际上就是告诉容器,不要使用默认的方法,使用我提供给你的方法
-
所以有其他博客说,工厂注册的使用时机:需要 复杂初始化,需要配置参数,根据条件创建对象时
-
!CAUTION
- 注册工厂时并不会立即执行 Lambda 表达式
- Lambda 只是被容器保存起来,
- 当获取服务(调用服务)时,容器才会执行该 Lambda 来创建对象
- 注册工厂时并不会立即执行 Lambda 表达式
-
c#
containerBuilder.AddSingleton<零件A>();
/// 这里是sp指IServiceProvider(当前容器)
containerBuilder.AddSingleton<产品>(sp =>
{
// 现在这个大括号里面的内容就是你自定义的对象创建过程
var part = sp.GetRequiredService<零件A>();
return new 产品(part);
});
shell
获取产品
↓
执行工厂函数
↓
从容器获取零件A
↓
创建并且返回产品
3.构建容器(BuildServiceProvider)
-
这个也不需要多讲,但是有一个特别需要注意的地方
-
!IMPORTANT
-
BuildServiceProvider()会根据当前注册信息生成真正的 DI 容器 -
换句话说,前面的创建集合,还并没有生成一个真正的DI容器
-
在调用之前:只有注册信息
-
调用之后:才能获取服务
-
-
c#
/* 三.生成容器 */
IServiceProvider container = containerBuilder.BuildServiceProvider();
containerBuilder.MakeReadOnly(); // 将生成类集合声明为只读,后续再添加服务会进行报错
// containerBuilder.AddSingleton<产品>(); //=> 使用MakeReadOnly后报错
4.获取服务(GetService)
- 一般情况下,我们会获取不带键的服务(如下),但是现在官方给了我们一种比较新的写法(带键)
c#
/* 四.服务调用(从容器获取对象) */
// 不带键
产品? product = container.GetService<产品>();
产品 product2 = container.GetRequiredService<产品>();
-
为什么需要带键?同一个服务类型,可能对应多个实现对象,我想指定实现对象,怎么办?
c#services.AddSingleton<IProtocol, ModbusProtocol>(); services.AddSingleton<IProtocol, OpcUaProtocol>(); // ...... // 两个实现对象,容器该怎么选择呢?会选择最后一个注册的实现 // 如果我想要选择中间的或者第一个怎么办呢? // 所以出现了带键的服务,注册时给它一个标签,获取时,指定对应标签,那就完事 var protocol = provider.GetRequiredService<IProtocol>();
4.1 不带键的服务
1)GetService
-
Get-获取,Service-服务,GetService-获取服务,但是你没说是不是必须的- 有就给,没有就算了(返回 null)
shell
# 如果产品存在 => 返回对象
# 如果产品不存在 => 返回 null
产品? product = container.GetService<产品>();
2)GetRequiredService
-
Required-必须的,今天你要是不把服务交出来,这个程序就别想跑了!- 必须要有,没有就直接报错
shell
# 如果产品存在 => 返回对象
# 如果产品不存在 => 返回 直接抛异常
产品 product = container.GetRequiredService<产品>();
4.2 带键的服务
说白了,和不带键的服务用法基本上一致,就是在注册时给它加了个标签
1)GetKeyedService
c#
/// 注册
containerBuilder.AddKeyedSingleton<产品>("产品");
// .......
/// 获取服务
// 获取产品
产品 product = container.GetRequiredKeyedService<产品>("产品");
2)GetRequiredKeyedService
c#
/// 注册
containerBuilder.AddKeyedSingleton<零件接口, 零件A>("零件A");
// .......
/// 获取服务
// 获取零件
零件接口 part = container.GetRequiredKeyedService<零件接口>("零件A");
四.流程图
-
感谢ChatGPT为我们生成了一张总的流程图(其实是我懒,不想画)
shell
IoC(思想)
↓
DI(实现方式)
↓
DI容器
# ================================
1.创建容器
↓
ServiceCollection
2.注册服务
↓
AddSingleton
AddTransient
AddScoped
3.构建容器
↓
BuildServiceProvider
4.获取服务
↓
GetRequiredService
GetService
五.实际案例
1.回顾
但是在此之前,我们先回顾一下内容,不然------
你不会以为就这么完了吗,我有说过结束了吗,那和我以前的博客有什么区别?
c#
注册(AddXXX)
↓
向容器登记对象创建规则
调用(GetXXX)
↓
让容器按照规则创建并返回对象
//========================================================
// 参考前面章节使用的代码(三.DI容器中的几个类)
// 去掉中间的步骤,单独使用这两组代码(注册+调用),会出现什么问题?
/* 第一组 */
services.AddSingleton<产品>();
产品 product = container.GetRequiredService<产品>();
/* 第二组 */
services.AddTransient<零件接口, 零件A>();
零件接口? part = container.GetService<零件接口>();
// 问:为什么这里的服务调用是 获取"零件接口"的对象 ,为什么不可以是使用"零件"的对象?
// 别瞎思考了,这里你看起来像对象的东西,实际上只是一个服务标识,即 注册为零件接口服务 的标识
1)注册到底注册了个什么东西出来?
-
注册阶段并不会创建对象,而是在告诉容器执行什么样的规则(以这里的两行代码为例):
-
以后如果有人需要产品,就创建产品对象
-
以后如果有人需要零件接口,就创建零件A对象
shell# 注册阶段 服务类型: 产品 零件接口 ↓ ↓ 实现类型: 产品类 零件A类 ######################################## # 类似于容器表 产品 零件接口 ↓ ↓ 产品类 零件A类 # 相当于(DI容器)说明书 我要什么服务 ↓ 应该创建什么对象 # 即:需要(服务类型)时,创建(实现类型)对象
-
2)当我们调用服务时,又干了什么事情?
-
你可能以为的:
shell# 产品 product = container.GetRequiredService<产品>(); 我要产品 => 查找登记表 => 找到产品类 => 查看产品构造函数 ↓ 发现产品需要零件接口 => 查找登记表 => 零件接口 -> 零件A ↓ 创建零件A ↓ 创建产品 ↓ 返回产品 # 零件接口? part = container.GetService<零件接口>(); 我要零件接口 => 查找登记表 => 零件接口 -> 零件A ↓ 创建零件A ↓ 返回零件A ↓ 调用GetID() -
但实际上我在上面埋了个雷
shellservices.AddSingleton<产品>(); 产品 product = container.GetRequiredService<产品>(); # 注意,这里只有一个单例注册,并没有services.AddTransient<零件接口, 零件A>(); 这个时候你获取服务会发生下面事件: 我要产品 -> 查找登记表 -> 找到产品类 -> 查看产品构造函数 ↓ 发现产品需要零件接口 -> 查找登记表 ↓ 未找到:零件接口 -> 某个实现 ↓ 无法创建零件接口对应对象 ↓ 无法创建产品 ↓ 报错 # 恭喜你,现在喜提编译器警告一次 System.InvalidOperationException:"Unable to resolve service for type 'DI_Demo.零件接口' while attempting to activate 'DI_Demo.产品'." -
为什么会报错?初学者可能会产生一个误解:
- 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?
- 问题是接口不是对象,他不能实例化
- 接口有实现吗,他有对象吗,它没对象啊,纯单身狗一条啊
- 接口只是一种约定,或者说草案,不是一个对象啊
- 所以会出现:产品需要零件接口,但是接口不能创建对象,然后就崩了
shell# 换句话说,你要是可以帮我实现下面这行代码算你厉害 ❌零件接口 part = new 零件接口(); # 容器真正要知道的是下面内容: 零件接口 ↓ 到底应该创建谁? ↓ 零件A,B,C中的哪一个? # 所以必须注册:明确知道是注册的谁 services.AddTransient<零件接口, 零件A>();
- 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?
-
但是,这个时候你可能还会非常疑惑:
-
那单例注册到底有什么用啊?为什么只放一个单例注册+一个单例注册的服务调用基本上无法使用?
-
单例注册往往不是用来解决依赖的,仅说明,该对象只创建一次
-
因为一个服务很少是孤立存在的,也因此单例注册几乎很少单独使用
-
一个实际案例
c#/// <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容器只是负责解析并创建这些依赖对象
shell# 很多很多教程都喜欢这样写: services.AddSingleton<产品>(); services.AddTransient<零件接口, 零件A>(); # 会让很多人误解: # Singleton负责产品,Transient负责依赖 # 但是假设我这样写,阁下如何应对呢 services.AddSingleton<产品>(); services.AddSingleton<零件接口, 零件A>();
-
2)误区2:认为单例注册是无法获取服务的
-
很多实际业务服务不会单独存在,但单例注册本身完全可以单独获取
-
而且当一堆单例注册出现的时候就不一样了 果然,还是人多力量大
-
多个服务注册在一起后,容器可以在获取服务时自动解析依赖关系(不仅局限于单例注册)
c#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>();shell我要 XXXService ↓ 发现需要 ConfigService ↓ 获取 ConfigService ########################### 发现需要 LogService ↓ 获取 LogService ########################### 创建 XXXService ↓ 返回 XXXService
-
3)误区3:认为使用注册必须一一对应
-
拜托,这是C#,不是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:注册时,若服务对象 ≠ 实现对象,获取服务时返回对象总认为是服务对象
c#
services.AddTransient<零件接口, 零件A>();
零件接口? part = container.GetService<零件接口>();
-
初学时,你以为的,创建了一个零件接口服务,然后返回了一个零件接口服务
-
不不不,这得看你实际代码,例如这里实际上返回的是零件A,只不过使用的服务是接口
c#
// 让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是设计模式
-
原代码:
c#/// 这里我们使用 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容器
c#
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:工厂模式
c#
/// <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容器,使用键
c#
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,你又会怎么设计
对于不同行业,不同领域的东西,你可以思考出怎么来做这个产品的能力
这就是我理解的业务能力
只要业务能力足够,很多人哪怕转行,也非常轻松
而不是像我现在这样,上班天天盯着电脑,你说做一个个人项目
自己都不知道自己的需求是什么,架构设计怎么做合适,我好像除了写点业务一无所有
但是当自己接到一个项目时,虽然架构不是特别熟悉,但是却好像明白我需要干什么
这依旧是值得思考的一个点,但是这些也只能后面慢慢来了