引言:什么是依赖注入,为什么需要它?
\n\n想象一下你在组装一台电脑。与其购买一台现成的一体机,你分别选择 CPU、显卡、内存,然后将它们连接到主板上。如果你想升级显卡,你不需要更换整台电脑——只需更换一个组件即可。依赖注入 (DI) 在编程中的工作原理与此相同。
\n\n依赖注入是一种设计模式,其中对象从外部获取其依赖项,而不是自己创建它们。在 JavaScript 和 Node.js 的上下文中,这意味着类或函数不会在内部创建其他类的实例,而是通过构造函数或参数接收它们。
\n\n这种方法是 SOLID 中 依赖反转原则 的一部分,它使代码更加灵活、可测试和可维护。在本文中,我们将通过面向初学者的实际示例来讲解 DI。
\n\n问题:代码的紧耦合
\n\n考虑 Node.js 应用程序中的典型情况。假设我们有一个用于发送电子邮件通知的服务:
\n\n// 没有依赖注入\nclass EmailService {\n sendEmail(to, subject, body) {\n console.log(`发送邮件至 ${to}: ${subject}`);\n // 实际的发送逻辑\n }\n}\n\nclass UserService {\n constructor() {\n this.emailService = new EmailService(); // ❌ 紧耦合\n }\n\n registerUser(email) {\n // 用户注册逻辑\n this.emailService.sendEmail(email, '欢迎!', '...');\n }\n}\n\nconst userService = new UserService();\nuserService.registerUser('user@example.com');\n\n\n问题出在哪里?UserService 类与 EmailService 的具体实现紧密耦合。如果你想:
- \n
- 将发送邮件替换为发送短信 \n
- 添加所有已发送消息的日志记录 \n
- 在不实际发送邮件的情况下测试
UserService\n
你将不得不重写 UserService 的代码。这违反了开闭原则。
解决方案:通过构造函数注入依赖
\n\n第一种也是最简单的依赖注入方法是通过类的构造函数传递它们。让我们重写我们的示例:
\n\n// 使用依赖注入\nclass EmailService {\n sendEmail(to, subject, body) {\n console.log(`发送邮件至 ${to}: ${subject}`);\n }\n}\n\nclass SmsService {\n sendSms(phone, message) {\n console.log(`发送短信至 ${phone}: ${message}`);\n }\n}\n\nclass UserService {\n constructor(notificationService) {\n this.notificationService = notificationService; // ✅ 依赖注入\n }\n\n registerUser(contact, isPhone = false) {\n // 注册逻辑\n if (isPhone) {\n this.notificationService.sendSms(contact, '欢迎!');\n } else {\n this.notificationService.sendEmail(contact, '欢迎!', '...');\n }\n }\n}\n\n// 使用\nconst emailService = new EmailService();\nconst userServiceWithEmail = new UserService(emailService);\nuserServiceWithEmail.registerUser('user@example.com');\n\nconst smsService = new SmsService();\nconst userServiceWithSms = new UserService(smsService);\nuserServiceWithSms.registerUser('+1234567890', true);\n\n\n现在 UserService 不知道具体使用的是哪个服务——邮件还是短信。它只是调用传入对象的 sendEmail 或 sendSms 方法。这被称为依赖反转:高层模块不依赖于低层模块,两者都依赖于抽象。
这种方法的优点:
\n- \n
- 灵活性:易于替换