引言:代码紧耦合的问题
每个Node.js开发者迟早都会遇到这样的情况:一个模块的修改会引发一连串其他十几个模块的改动。在编写单元测试时,这个问题尤为突出:为了测试一个服务,不得不启动数据库、外部API和缓存。问题的根源在于紧耦合。
依赖注入(DI)是一种解决此问题的设计模式。类或函数不再自行创建其依赖项,而是从外部获取它们。这使得代码更加灵活、可测试,并符合SOLID原则中的依赖反转原则。
在本文中,我们将从实践角度剖析DI:从简单的手动注入到在Node.js中使用专业容器。
1. 手动依赖注入:第一步
糟糕的方法:紧耦合
考虑一个典型的发送电子邮件的服务示例:
// userService.jsconst emailService = require('./emailService');
class UserService { async registerUser(email) { // 对具体实现的紧耦合 await emailService.sendWelcomeEmail(email); console.log('用户已注册'); }}
module.exports = UserService;问题:在测试UserService时,无法将emailService替换为模拟对象。必须使用像proxyquire或jest.mock这样的复杂工具。
好的方法:通过构造函数注入
重写服务,通过构造函数传递依赖项:
// userService.jsclass UserService { constructor(emailService) { this.emailService = emailService; }
async registerUser(email) { await this.emailService.sendWelcomeEmail(email); console.log('用户已注册'); }}
module.exports = UserService;现在我们可以轻松地测试该服务:
// userService.test.jsconst UserService = require('./userService');
const mockEmailService = { sendWelcomeEmail: jest.fn().mockResolvedValue(true)};
test('应该发送欢迎邮件', async () => { const service = new UserService(mockEmailService); await service.registerUser('test@test.com'); expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@test.com');});2. JavaScript中的依赖注入类型
在JavaScript/Node.js中,有三种主要的依赖注入方式:
2.1. 构造函数注入
最常用且最可预测的方式。依赖项在创建对象时传递:
class DatabaseService { constructor(connection) { this.connection = connection; }
async query(sql) { return this.connection.execute(sql); }}
// 使用const db = new DatabaseService(new MySQLConnection());2.2. Setter注入
适用于依赖项可以在对象运行过程中更改的情况:
class LoggerService { setLogger(logger) { this.logger = logger; }
log(message) { this.logger.write(`[${new Date().toISOString()}] ${message}`); }}
const logger = new LoggerService();logger.setLogger(new ConsoleLogger());logger.log('应用程序已启动');2.3. 函数参数注入
非常适合函数式方法和工具函数:
function sendNotification(user, emailService, smsService) { emailService.send(user.email, '通知'); smsService.send(user.phone, '通知');}
// 测试const mockEmail = { send: jest.fn() };const mockSms = { send: jest.fn() };sendNotification({ email: 'a@b.com', phone: '123' }, mockEmail, mockSms);