为什么我必须在 JS 中使用依赖注入?
这为什么有用?
结论
每当我们着手一个项目,无论是现有的还是新建的,我们总是会思考如何让项目更易于管理、更可扩展、更易于测试。这时,依赖注入就派上用场了。
但首先,我们所说的依赖注入是什么意思?
它是一种软件设计模式,通过将获取依赖项的责任移到依赖它的代码之外,我们可以使代码易于单元测试。它也指将代码中某些部分的依赖项提供给依赖这些依赖项的对象、函数或模块。
这为什么有用?
如前所述,如果我们抽象出代码片段,在需要时了解它们所需的具体依赖关系,就能使代码片段更容易测试。例如:
//File: services/notifications/index.js
import User from '../database/models/user';
import { logError } from './logger';
import { sendEmailNotification } from './emails';
const DEFAULT_NOTIFICATION_MESSAGE = 'Hi, friend. :)';
export const sendNotificationsToUsers = async (ids = []) => {
try {
const users = await User.find({
id: ids
});
const promises = users.map(({
email,
// This we'll add notifications into a queue to process them in the background.
// Don't freak out.
}) => sendEmailNotification(email, DEFAULT_NOTIFICATION_MESSAGE));
await Promise.all(promises);
return {
success: true
};
} catch (e) {
logError(e);
return {
success: false
};
}
};
在前面的例子中,我们尝试向某些用户发送通知。这没什么奇怪的。但是,为了测试这个功能,我们需要做什么呢?模拟这三个依赖项以便将其作为一个单元进行测试,是否容易?
对我来说,不是。
我该怎么办?
这里有两种情况。第一种,模块中只有这个函数需要依赖项。第二种,模块中的所有函数都需要这些依赖项。
对于第一种情况:
//File: services/notifications/index.js
const DEFAULT_NOTIFICATION_MESSAGE = 'Hi, friend. :)';
export const sendNotificationsToUsers = async ({
User,
logger,
notifier
}, ids = []) => {
try {
const users = await User.find({
id: ids
});
const promises = users.map((user => notifier.notifyUser(user, DEFAULT_NOTIFICATION_MESSAGE)));
await Promise.all(promises);
return {
success: true
};
} catch (e) {
logger.logError(e);
return {
success: false
};
}
};
我们在这里做了一些重构:
- 我们将依赖项作为函数中的第一个配置参数传递
sendNotificationsToUsers
。 - 我们允许函数不关心需要哪种类型的记录器或通知器,这样该函数就可以通用,并且可以在将来重复使用。比如使用短信通知,或者任何我们想到的。
现在这段代码是可测试的,并且可以模拟依赖项:
//some test file
import assert from 'assert';
import {
sendNotificationsToUsers
}
from '../core/services/notifications';
describe('Notification service', () => {
const mockUserDB = {
find() {
return Promise.resolve([{
email: 'some-email@gmail.com',
phone: 'some-phone-number'
}]);
}
};
const logger = {
logError(e) {
console.log(e);
}
}
describe('#sendNotificationsToUsers', () => {
it('can send notifications via emails', async () => {
const notifier = {
notifyUser(_user, _message) {
return Promise.resolve(true);
}
};
const notificationResponse = await sendNotificationsToUsers({
User: mockUserDB,
logger,
notifier,
}, [1]);
assert(notificationResponse, 'Notifications failed to be sent.');
});
});
});
那么整个模块要求依赖关系怎么样?
我们只需将模块导出为接受这些依赖项的函数并按如下方式使用它:
export default ({
User,
logger,
notifier
}) => ({
async sendNotificationsToUsers(ids = []) {
try {
const users = await User.find({
id: ids
});
const promises = users.map((user => notifier.notifyUser(user, DEFAULT_NOTIFICATION_MESSAGE)));
await Promise.all(promises);
return {
success: true
};
} catch (e) {
logger.logError(e);
return {
success: false
};
}
}
});
//Usage
import User from 'services/users';
import logger from 'services/logger';
import notifier from 'services/emails';
import getNotificationsService from 'services/notifications';
const { sendNotificationsToUsers } = getNotificationsService({ User, logger, notifier });
sendNotificationsToUsers([1, 2, 3]);
结论
我相信这种编码方式对我们所有人都有帮助,它将帮助我们将模块编写为真正的单元,也将帮助我们在测试和开发时提高工作效率。
Please share your thoughts, corrections or comments below and until the next time. Happy Coding.