引言:什么是依赖注入以及为什么需要它
依赖注入(DI)是实现SOLID中依赖倒置原则的关键设计模式之一。在实践中,这意味着对象不会自行创建其依赖项,而是从外部获取它们。这使得代码灵活、可测试且松散耦合。
在C#和.NET生态系统中,DI已成为事实上的标准。从.NET Core开始,平台内置了自身的依赖注入容器,用于ASP.NET Core、后台服务甚至控制台应用程序。不理解DI就无法想象现代C#开发。
在本文中,我们将通过实际示例分析DI:从手动注入到使用内置的.NET容器。您将看到如何消除紧耦合,并使代码适合单元测试。
没有依赖注入的问题
设想一个典型场景:OrderService类处理订单并发送电子邮件通知。没有DI的代码如下所示:
public class EmailSender{ public void Send(string to, string subject, string body) { Console.WriteLine($"发送电子邮件至 {to}: {subject}"); }}
public class OrderService{ private EmailSender _emailSender;
public OrderService() { // 紧耦合——类自行创建依赖项 _emailSender = new EmailSender(); }
public void ProcessOrder(int orderId) { // 订单处理逻辑... _emailSender.Send("user@example.com", "订单已确认", $"您的订单 #{orderId} 已接收。"); }}问题在哪里?OrderService紧耦合于EmailSender的具体实现。如果我们想将电子邮件发送替换为短信或日志记录,就必须修改OrderService的代码。测试也变得复杂:无法将真实的EmailSender替换为模拟对象。
解决方案:通过构造函数注入依赖项
迈向DI的第一步是提取抽象(接口)并通过构造函数传递依赖项。这称为构造函数注入。
步骤1:创建接口
public interface INotificationService{ void Send(string to, string subject, string body);}步骤2:实现接口
public class EmailSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"电子邮件已发送:{subject} -> {to}"); }}
public class SmsSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"短信已发送:{subject} -> {to}"); }}步骤3:通过构造函数注入依赖项
public class OrderService{ private readonly INotificationService _notificationService;
// 依赖项从外部传入 public OrderService(INotificationService notificationService) { _notificationService = notificationService; }
public void ProcessOrder(int orderId) { // 订单处理逻辑... _notificationService.Send("user@example.com", "订单已确认", $"您的订单 #{orderId} 已接收。"); }}现在OrderService不知道具体使用的是哪种实现。我们可以传入EmailSender、SmsSender或任何其他实现INotificationService的类。代码变得灵活且可测试。
在.NET中使用内置的IoC容器
手动创建所有依赖项并不方便。.NET有一个内置容器(IoC容器),可以自动管理对象的生命周期。让我们看一个控制台应用程序的示例。