1. 依赖注入的本质与核心价值
依赖注入(Dependency Injection,简称DI)是一种设计模式,它通过外部实体将依赖关系传递给使用它们的组件。这种模式的核心在于"控制反转"(Inversion of Control,IoC)——将对象的创建和管理权从对象内部转移到外部容器。
想象你正在组装一台电脑。传统方式下,你需要自己购买所有零部件并手动组装(相当于在类内部直接new对象)。而依赖注入就像是一家专业的电脑组装服务——你只需要告诉服务商你需要什么配置的电脑(声明依赖),服务商会自动为你组装好并送货上门(注入依赖)。
这种模式带来了三个显著优势:
- 解耦性:组件不再需要知道如何创建或配置它们的依赖项
- 可测试性:可以轻松地用模拟对象替换真实依赖进行单元测试
- 可维护性:依赖关系的变更只需修改配置,而不需要修改使用这些依赖的代码
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 依赖注入的三种实现方式
2.1 构造函数注入
这是最推荐的方式,通过类的构造函数来注入依赖。这种方式明确声明了类运行所需的全部依赖,且保证了对象在创建后就是完整可用的状态。
csharp复制public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
}
}
注意:在C#中,建议将注入的依赖标记为readonly,防止在类内部被意外修改。
2.2 属性注入
通过公共属性来注入依赖,这种方式更加灵活但缺乏强制性——类在使用依赖前需要检查它们是否已被注入。
csharp复制public class ProductService
{
public IProductRepository Repository { get; set; }
public void UpdateProduct(Product product)
{
if(Repository == null)
throw new InvalidOperationException("Repository must be set");
Repository.Update(product);
}
}
2.3 方法注入
仅在特定方法需要时才注入依赖,适用于那些不常用或可选的依赖。
csharp复制public class ReportGenerator
{
public void Generate(ReportData data, IReportFormatter formatter)
{
var report = formatter.Format(data);
// 生成报告逻辑
}
}
3. C#中的依赖注入实践
3.1 .NET Core/5+的内置DI容器
现代.NET提供了轻量级但功能完备的DI容器:
csharp复制// 在Startup.cs或Program.cs中配置服务
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddTransient<IEmailService, SmtpEmailService>();
services.AddSingleton<ICacheManager, MemoryCacheManager>();
// 使用服务
public class CheckoutController : Controller
{
private readonly IOrderRepository _repository;
public CheckoutController(IOrderRepository repository)
{
_repository = repository;
}
// 控制器方法
}
三种生命周期:
- Transient:每次请求都创建新实例
- Scoped:在同一作用域内重用实例(如一次Web请求)
- Singleton:整个应用程序生命周期内使用单一实例
3.2 第三方DI容器对比
虽然内置容器能满足大部分需求,但在复杂场景下可能需要更强大的第三方容器:
| 特性 | 内置容器 | Autofac | SimpleInjector | Ninject |
|---|---|---|---|---|
| 属性注入 | ❌ | ✔ | ✔ | ✔ |
| 子容器/作用域 | 基础 | 强大 | 中等 | 中等 |
| 性能 | 优秀 | 优秀 | 极佳 | 良好 |
| 条件注册 | 有限 | 丰富 | 丰富 | 中等 |
| 学习曲线 | 简单 | 中等 | 中等 | 中等 |
对于大多数应用,建议从内置容器开始,只有在确实需要高级功能时才考虑第三方方案。
4. 依赖注入的进阶模式与技巧
4.1 装饰器模式增强功能
依赖注入可以优雅地实现装饰器模式,在不修改原有类的情况下增强功能:
csharp复制// 基础服务
public interface IDataService
{
Data GetData();
}
// 装饰器实现缓存
public class CachingDataService : IDataService
{
private readonly IDataService _inner;
private readonly ICache _cache;
public CachingDataService(IDataService inner, ICache cache)
{
_inner = inner;
_cache = cache;
}
public Data GetData()
{
var cacheKey = "data-key";
if(_cache.TryGet(cacheKey, out Data cached))
return cached;
var data = _inner.GetData();
_cache.Set(cacheKey, data);
return data;
}
}
// 注册时自动包装
services.AddScoped<IDataService, RealDataService>();
services.Decorate<IDataService, CachingDataService>();
4.2 工厂模式解决复杂创建逻辑
当对象创建过程复杂时,可以结合抽象工厂模式:
csharp复制public interface IDbConnectionFactory
{
IDbConnection CreateConnection(string connectionName);
}
public class SqlConnectionFactory : IDbConnectionFactory
{
private readonly IConfiguration _config;
public SqlConnectionFactory(IConfiguration config)
{
_config = config;
}
public IDbConnection CreateConnection(string connectionName)
{
var connStr = _config.GetConnectionString(connectionName);
var connection = new SqlConnection(connStr);
// 可能的其他初始化逻辑
return connection;
}
}
// 使用
public class ReportRepository
{
private readonly IDbConnectionFactory _factory;
public ReportRepository(IDbConnectionFactory factory)
{
_factory = factory;
}
public Report GetReport(int id)
{
using var conn = _factory.CreateConnection("Reports");
// 查询逻辑
}
}
5. 常见陷阱与最佳实践
5.1 服务定位器反模式
避免这种"伪DI"做法:
csharp复制// 反模式 - 服务定位器
public class BadService
{
public void DoWork()
{
var logger = ServiceLocator.GetService<ILogger>();
logger.Log("Working...");
}
}
这种方式的问题在于:
- 隐藏了类对ILogger的依赖
- 使单元测试变得困难
- 可能导致运行时错误而非编译时错误
5.2 循环依赖问题
当ServiceA依赖ServiceB,而ServiceB又依赖ServiceA时,DI容器会抛出异常。解决方案包括:
- 重构设计,提取公共逻辑到第三个服务
- 将其中一个依赖改为方法注入
- 使用接口分离
5.3 过度注入问题
当一个构造函数需要注入太多依赖(如超过5个),通常意味着类承担了太多职责。这时应该考虑:
- 使用Facade模式合并相关服务
- 拆分当前类为多个更专注的类
- 检查是否违反了单一职责原则
5.4 日志记录的最佳实践
对于日志记录这种几乎每个服务都需要的基础设施,可以这样优化:
csharp复制// 基础日志接口
public interface ILogger<T> where T : class
{
void LogInformation(string message);
}
// 实现
public class ConsoleLogger<T> : ILogger<T> where T : class
{
public void LogInformation(string message)
{
Console.WriteLine($"[{typeof(T).Name}] {message}");
}
}
// 自动注册
services.AddSingleton(typeof(ILogger<>), typeof(ConsoleLogger<>));
// 使用
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void Process()
{
_logger.LogInformation("Processing order...");
}
}
这种泛型注册方式可以自动为每个类提供类型化的日志实例,同时保持轻量级的注册开销。
