在学习 .NET 开发过程中,很多开发者都会接触到一个概念:依赖注入(Dependency Injection,简称 DI)。
特别是在 ASP.NET Core 项目中,依赖注入几乎是必不可少的一部分。
但是很多初学者会有一个疑问:
为什么需要依赖注入?
以前直接 new 一个对象不是也可以正常使用吗?
实际上,依赖注入并不是为了让代码变复杂,而是为了解决项目不断扩大后,代码之间耦合越来越严重的问题。
本文将通过几个实际场景,介绍 .NET 中依赖注入解决的问题,以及它在实际项目中的使用方式。
一、传统写法存在的问题
假设我们有一个用户服务:
public class UserService
{
public void GetUserInfo()
{
Console.WriteLine("获取用户信息");
}
}
然后在业务代码中直接创建:
public class UserController
{
private UserService userService;
public UserController()
{
userService = new UserService();
}
public void Query()
{
userService.GetUserInfo();
}
}
这种写法非常直观。
Controller 需要 UserService,那么直接 new 一个即可。
但是随着项目发展,会出现一些问题。
例如:
以后 UserService 需要连接数据库:
public class UserService
{
private readonly Database database;
public UserService()
{
database = new Database();
}
}
然后 Database 又依赖配置文件:
public class Database
{
private string connectionString;
public Database()
{
connectionString = "数据库地址";
}
}
最后可能形成:
Controller
↓
UserService
↓
Database
↓
Configuration
每一层都需要自己创建下一层对象。
项目越大,代码之间的关系越复杂。
这就是所谓的:
高耦合。
二、依赖注入解决了对象创建的问题
依赖注入的核心思想:
不要让业务代码主动创建对象,而是由外部统一管理对象。
例如:
原来的方式:
UserController
创建 UserService
创建 Database
创建 Config
变成:
.NET 容器
创建 Config
创建 Database
创建 UserService
注入 UserController
Controller 不关心 UserService 怎么创建。
它只需要声明:
“我需要一个 UserService”。
修改代码:
public class UserController
{
private readonly UserService userService;
public UserController(UserService service)
{
userService = service;
}
public void Query()
{
userService.GetUserInfo();
}
}
可以看到:
Controller 不再负责创建 UserService。
它只负责使用。
这就是依赖注入最核心的作用:
将对象创建和业务使用分离。
三、.NET中的依赖注入容器
.NET Core 内置了依赖注入容器。
通常在 Program.cs 中注册服务。
例如:
builder.Services.AddTransient<UserService>();
表示:
当程序需要 UserService 时,自动创建一个对象。
然后 Controller 中:
public UserController(UserService service)
{
}
框架会自动传入。
常见注册方式有三种:
1. Transient(瞬时)
services.AddTransient<UserService>();
每次请求创建新的对象。
适合:
普通业务服务
无状态工具类
例如:
数据转换工具。
2. Scoped(作用域)
services.AddScoped<UserService>();
在一次请求生命周期内保持同一个对象。
适合:
Web接口
数据库上下文
例如:
一个 HTTP 请求过程中,多次访问数据库,希望使用同一个 DbContext。
3. Singleton(单例)
services.AddSingleton<UserService>();
整个程序生命周期只有一个对象。
适合:
配置管理
缓存
全局服务
例如:
设备连接管理器:
public class DeviceManager
{
}
整个程序只需要一个。
四、依赖注入最大的价值:方便替换实现
假设程序需要发送消息。
最开始:
public class MessageService
{
public void Send()
{
Console.WriteLine("发送短信");
}
}
业务代码:
var service = new MessageService();
service.Send();
如果以后需求改变:
短信改成邮件。
那么以前所有调用:
new MessageService()
都需要修改。
使用接口后:
定义:
public interface IMessageService
{
void Send();
}
短信实现:
public class SmsService : IMessageService
{
public void Send()
{
Console.WriteLine("发送短信");
}
}
邮件实现:
public class EmailService : IMessageService
{
public void Send()
{
Console.WriteLine("发送邮件");
}
}
注册:
services.AddTransient<IMessageService, EmailService>();
业务代码:
public class OrderService
{
private readonly IMessageService messageService;
public OrderService(IMessageService service)
{
messageService = service;
}
}
以后只需要修改注册:
services.AddTransient<IMessageService, SmsService>();
业务代码完全不用修改。
这就是依赖注入带来的扩展能力。
五、依赖注入在实际项目中的常见场景
在真实 .NET 项目中,依赖注入经常用于:
数据访问层
例如:
services.AddScoped<IUserRepository, UserRepository>();
业务层只依赖接口:
public class UserService
{
private readonly IUserRepository repository;
public UserService(
IUserRepository repository)
{
this.repository = repository;
}
}
以后数据库从 MySQL 换成 SQL Server。
只需要替换实现。
日志服务
例如:
services.AddLogging();
业务代码:
public class DeviceService
{
private readonly ILogger logger;
public DeviceService(
ILogger<DeviceService> logger)
{
this.logger = logger;
}
}
不用自己创建日志对象。
硬件设备通信
对于上位机开发,例如:
串口通信:
ISerialPortService
蓝牙通信:
IBluetoothService
设备协议解析:
IProtocolParser
都可以通过依赖注入管理。
例如:
services.AddSingleton<IProtocolParser, PacketParser>();
主程序只负责调用。
六、依赖注入是不是一定更好?
依赖注入并不是所有场景都必须使用。
如果只是一个简单的小工具:
var tool = new FileHelper();
直接创建完全没有问题。
但是当项目出现:
多个模块
多种实现
长期维护
多人协作
单元测试需求
依赖注入的优势就会越来越明显。
它本质上解决的是:
如何让大型项目中的对象管理更加清晰。
对于 .NET 开发来说,理解依赖注入不仅能够帮助开发 ASP.NET Core 项目,也能够帮助理解桌面客户端、上位机软件等大型应用的架构设计。
在后续开发 C# 上位机程序时,例如设备管理、通信模块、协议解析模块,也会经常使用这种设计方式。