依赖注入(DI)是 ASP.NET Core 中的一个基本实践,它让开发人员能够构建模块化、可测试和易于维护的应用程序。虽然构造函数注入是最常见的依赖注入方式,但方法注入也具有一些常被忽视的优势。
这篇文章探讨了为什么在某些情况下,方法注入可能比构造器注入更好,以及它如何解决一些通常与构造器注入相关的问题。然而,重要的是要澄清,方法注入并不能替代良好的类设计——如果一个类变得过大或违反单一职责原则(SRP),最好的方法是应该将其拆分成更小、更专注的组件,而不是只是改变注入方式。
构造函数注入的那些事儿构造器注入是指通过类构造函数传递依赖。
public class OrderService
{
private readonly IPaymentGateway _paymentGateway;
private readonly INotificationService _notificationService;
public OrderService(IPaymentGateway paymentGateway, INotificationService notificationService)
{
_paymentGateway = paymentGateway ?? throw new ArgumentNullException(nameof(paymentGateway));
_notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
}
public void ProcessOrder(Order order)
{
_paymentGateway.Process(order);
_notificationService.Notify(order);
}
}
解释:OrderService
类负责处理订单,它依赖于 IPaymentGateway
和 INotificationService
。构造函数接收这两个参数,并确保它们不为空。ProcessOrder
方法使用支付网关处理订单,并用通知服务通知订单状态。
虽然构造器注入很有效且实用,但还是有几个缺点的。
1. 构造函数重载与类膨胀问题- 带有众多依赖的长构造函数使得类难以阅读、维护和测试。
- 它并不违反单一职责原则,但在注入过多的无关依赖的情况下,可能表明该类功能过多,需要重构。
- 如果所有依赖关系在每个方法中都是必需的,构造函数注入仍然是最好的选择。
- 如果单例服务依赖于作用域为scoped或瞬时的依赖项,ASP.NET Core会在运行时因服务生命周期冲突而抛出错误。
-
示例(例如):
- 例如:
services.AddSingleton<OrderService>();
services.AddScoped<IPaymentGateway, PaymentGateway>(); // 单例容器中的作用域依赖
- 服务生命周期不匹配是IoC容器配置中的问题,而不是选择DI方法。然而,方法注入只在需要时解决短暂依赖,避免了不当的生命周期保留。
- 构造函数注入使注入模拟对象变得简单,但方法注入可以通过限制每个测试用例所需的依赖而不是需要模拟不必要的对象来简化单元测试。
- 如果有不必要的依赖导致测试变得复杂,可以考虑重构类以减少多余的依赖,这可能是一个更好的选择。
- 构造函数没有指明每个方法所需的依赖项。这不是它的职责——它只是为对象提供初始状态。
- 方法注入使得每个方法使用的依赖项更加明确,提高了可读性。
- 有关封装的问题——方法注入更明确地揭示了内部实现细节。如果这是一个问题,可以考虑将这些大类拆分成更小的单一职责组件。
依赖注入通过按需传递依赖,而不是在对象构建时传递,从而解决了这些问题。
方法注入例子: public class 订单服务类
{
public void 处理订单请求(订单订单对象, IPaymentGateway 支付处理接口, INotificationService 通知处理接口)
{
支付处理接口处理订单(order对象);
通知处理接口通知订单(order对象);
}
}
方法注入什么时候有用?
✅ 避免构造函数过载 — 这在某些依赖项仅在特定方法中需要时非常有用。
✅ 防止生命周期问题 — 作用域和服务可以是瞬态的,在运行时解析它们不会有不匹配的问题。
✅ 简化测试性 — 每个测试用例只需要相关的依赖项,减少了不必要的模拟。
✅ 提高可维护性 — 清晰地显示每个方法的依赖项,使得代码库更加易读。
如何在ASP.NET Core中使用方法注入(Method Injection)?
1. 直接在方法中注入dependencies适用于无状态的服务,或当不总是需要的依赖时。
public class ReportService
{
// 用于生成报表的服务
public void GenerateReport(IReportGenerator reportGenerator)
{
// 生成报表的方法
// 用于生成报表的生成器
reportGenerator.Generate();
// 调用生成器生成报表
}
}
2. 通过中间件注入依赖
在处理ASP.NET Core中间件时,如果每次请求的依赖关系都不一样,这个功能非常有用。
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, ILogger<LoggingMiddleware> logger)
{
logger.LogInformation("正在处理请求...");
await _next(context);
}
}
3. 在API控制器中通过方法注入依赖
简化ASP.NET Core控制器中的构造函数依赖项,使其更简洁。
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpPost]
public IActionResult 处理订单([FromBody] OrderDto 订单, [FromServices] IPaymentGateway 支付网关)
{
支付网关.Process(订单);
return Ok();
}
}
我们是否应该不再使用构造器注入?
不一定。当构造器注入仍然有用时:
- 每个方法都需要依赖。
- 依赖的数量是可以管理的(1-3个)。
- 没有生命周期冲突的问题(比如,单例注入范围服务)。
然而,对于依赖项众多、可选的服务或每个方法有不同的依赖项的服务而言,方法注入技术是更好的选择。因此,方法注入技术是更好的选择。
.NET 最新参考资料和文档 最后的感想不论是构造器注入和方法注入都有其应用场景。选择取决于你的具体需求。
- 当整个类始终需要所有依赖项时,请使用构造函数注入。
- 当依赖项因方法不同或造成生命周期问题时,请使用方法注入。
- 如果过多的依赖项表明单一责任原则(SRP)被违反,请重构大型类。
如果你一直在为构造器臃肿或服务生命周期问题而烦恼,不妨试试方法注入,亲自体验其带来的好处。然而,首先考虑合理的类设计更为重要,选择注入方法次之,良好的代码结构才是关键。
这篇文章对你有帮助吗?
如果你喜欢这篇关于设计模式的文章,就点个赞吧!
关注我一下,在Medium上可以获得更多关于.NET架构和性能的见解。
订阅我,这样你就不会错过任何一篇新文章,还可以开启邮件通知 🔔!
- 提升你的 C# 代码:实现责任链、结果和构建器工厂模式
- 你应该尽快修复的 .NET 性能反模式 Top 10
- C# 中通过智能缓存提高 API 性能:Replicant 如何提升 HttpClient 的性能
这篇文章受到了许多ASP.NET Core开发人员在实际工作中遇到的问题的启发。如果它对你有帮助,欢迎连接到领英,了解更多关于.NET开发的内容!