我是来自.NET世界的NodeJs开发的新手 我正在网上搜索在Javascript中重新评估DI / DIP的最佳做法
在.NET中,我会在构造函数中声明我的依赖项,而在javascript中,我看到一个常见的模式是通过require语句在模块级别声明依赖项。
对我来说,当我使用require时,我会使用构造函数来接收我的依赖项,这样我就可以更加灵活。
你建议你做什么作为javascript的最佳实践?(我正在寻找架构模式,而不是IOC技术解决方案)
在网上搜索我发现了这篇博文(在评论中有一些非常有趣的讨论): https://blog.risingstack.com/dependency-injection-in-node-js/
它很好地总结了我的冲突。 这里是博客文章中的一些代码,让您了解我在说什么:// team.js
var User = require('./user');
function getTeam(teamId) {
return User.find({teamId: teamId});
}
module.exports.getTeam = getTeam;
一个简单的测试看起来像这样:
// team.spec.js
var Team = require('./team');
var User = require('./user');
describe('Team', function() {
it('#getTeam', function* () {
var users = [{id: 1, id: 2}];
this.sandbox.stub(User, 'find', function() {
return Promise.resolve(users);
});
var team = yield team.getTeam();
expect(team).to.eql(users);
});
});
VS DI:
// team.js
function Team(options) {
this.options = options;
}
Team.prototype.getTeam = function(teamId) {
return this.options.User.find({teamId: teamId})
}
function create(options) {
return new Team(options);
}
试验:
// team.spec.js
var Team = require('./team');
describe('Team', function() {
it('#getTeam', function* () {
var users = [{id: 1, id: 2}];
var fakeUser = {
find: function() {
return Promise.resolve(users);
}
};
var team = Team.create({
User: fakeUser
});
var team = yield team.getTeam();
expect(team).to.eql(users);
});
});
答案 0 :(得分:5)
关于你的问题:我不认为JS社区有一个共同的做法。我在野外看到过这两种类型,需要修改(如rewire或proxyquire)和构造函数注入(通常使用专用的DI容器)。但是,就个人而言,我认为不使用DI容器更适合JS。这是因为JS是functions as first-class citizens的动态语言。让我解释一下:
使用DI容器强制构造函数注入用于所有内容。由于两个主要原因,它会产生巨大的配置开销:
关于第一个参数:我不会仅仅为我的单元测试调整我的代码。如果它使您的代码更清晰,更简单,更通用,更不容易出错,那就去吧。但如果你唯一的理由是你的单元测试,我不会采取权衡。您可以通过需要修改和monkey patching获得相当远的距离。如果你发现自己写了太多的模拟,你可能根本不应该编写单元测试,而是进行集成测试。 Eric Elliott写了关于这个问题的a great article。
关于第二个参数:这是一个有效的参数。如果你想创建一个只关心接口的组件,而不是关于实际的实现,我会选择一个简单的构造函数注入。但是,由于JS不会强迫您使用类,所以为什么不使用函数呢?
在functional programming中,将状态IO与实际处理分离是一种常见的范例。例如,如果您正在编写应该计算文件夹中文件类型的代码,那么可以写一个(特别是当他/她来自一种强制执行各种类的语言时):
const fs = require("fs");
class FileTypeCounter {
countFileTypes(dirname, callback) {
fs.readdir(dirname, function (err) {
if (err) return callback(err);
// recursively walk all folders and count file types
// ...
callback(null, fileTypes);
});
}
}
现在,如果你想测试它,你需要更改代码以注入假的fs
模块:
class FileTypeCounter {
constructor(fs) {
this.fs = fs;
}
countFileTypes(dirname, callback) {
this.fs.readdir(dirname, function (err) {
// ...
});
}
}
现在,每个使用您的类的人都需要将fs
注入构造函数中。由于这很枯燥,并且一旦你有很长的依赖图,你的代码会变得更复杂,开发人员发明了DI容器,他们只需要配置东西,DI容器就可以找出实例。
然而,仅仅编写纯函数呢?
function fileTypeCounter(allFiles) {
// count file types
return fileTypes;
}
function getAllFilesInDir(dirname, callback) {
// recursively walk all folders and collect all files
// ...
callback(null, allFiles);
}
// now let's compose both functions
function getAllFileTypesInDir(dirname, callback) {
getAllFilesInDir(dirname, (err, allFiles) => {
callback(err, !err && fileTypeCounter(allFiles));
});
}
现在你有两个超常用的功能,一个是IO,另一个是处理数据。 fileTypeCounter
是pure function,非常容易测试。 getAllFilesInDir
不纯,但是这是一项非常常见的任务,您经常会在npm上找到它,其他人已经为此编写了集成测试。 getAllFileTypesInDir
只需用一点控制流来编写你的函数。这是集成测试的典型案例,您希望确保整个应用程序正常运行。
通过在IO和数据处理之间分离代码,您根本不需要注入任何东西。如果你不需要注射任何东西,这是一个好兆头。纯函数是最容易测试的函数,并且仍然是在项目之间共享代码的最简单方法。
答案 1 :(得分:3)
过去,我们从Java和.NET了解的DI容器不存在。有了Node 6,ES6 Proxies开辟了这种容器的可能性 - 例如Awilix。
因此,让我们将您的代码重写为现代ES6。
class Team {
constructor ({ User }) {
this.User = user
}
getTeam (teamId) {
return this.User.find({ teamId: teamId })
}
}
测试:
import Team from './Team'
describe('Team', function() {
it('#getTeam', async function () {
const users = [{id: 1, id: 2}]
const fakeUser = {
find: function() {
return Promise.resolve(users)
}
}
const team = new Team({
User: fakeUser
})
const team = await team.getTeam()
expect(team).to.eql(users)
})
})
现在,使用Awilix,让我们编写组合根:
import { createContainer, asClass } from 'awilix'
import Team from './Team'
import User from './User'
const container = createContainer()
.register({
Team: asClass(Team),
User: asClass(User)
})
// Grab an instance of Team
const team = container.resolve('Team')
// Alternatively...
const team = container.cradle.Team
// Use it
team.getTeam(123) // calls User.find()
这很简单; Awilix也可以处理对象的生命周期,就像.NET / Java容器一样。这可以让你做一些很酷的事情,比如将当前用户注入你的服务,根据http请求对你的服务进行一次实例化等等。