Javascriptdependency injection和节点中的DIP:require vs构造函数注入

我是来自.NET世界的NodeJs开发新手,我正在search网页中的最佳实践,重新调整Javascript中的DI / DIP

在.NET中,我将声明我的依赖在构造函数,而在JavaScript中,我看到一个常见的模式是通过一个require语句在模块级别声明依赖关系。

对我来说,它看起来像当我使用要求我被耦合到一个特定的文件,而使用构造函数来接收我的依赖是更加灵活。

你会推荐做什么,作为在JavaScript的最佳做法? (我正在寻找build筑模式,而不是国际奥委会的技术解决scheme)

searchnetworking我来到这个博客文章(这在评论中有一些非常有趣的讨论): 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; 

一个简单的testing看起来像这样:

  // 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); } 

testing:

 // 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); }); }); 

关于你的问题:我不认为JS社区有一个共同的做法。 我见过这两种types,需要修改(如rewire或proxyquire )和构造注入(通常使用专用的DI容器)。 但是,我个人认为不使用DI容器更适合于JS。 这是因为JS是一种dynamic的语言,具有一stream的公民的function 。 让我解释一下:

使用DI容器强制构造器注入一切 。 它造成了巨大的configuration开销,主要有两个原因:

  1. 在unit testing中提供模拟
  2. 创build对环境一无所知的抽象组件

关于第一个参数 :我不会为我的unit testing调整我的代码。 如果它使你的代码更干净,更简单,更通用,更容易出错,那就出去吧。 但是,如果你唯一的原因是你的unit testing,我不会采取权衡。 你可以得到相当远的需要修改和猴子修补 。 如果你发现自己写太多的模拟,你可能根本不应该写一个unit testing,而是一个集成testing。 Eric Elliott写了一篇关于这个问题的伟大文章 。

关于第二个参数 :这是一个有效的参数。 如果你想创build一个只关心接口但不关心实际实现的组件,我会select一个简单的构造器注入。 但是,由于JS并不强迫你使用类,为什么不使用函数呢?

在函数式编程中 ,将状态IO与实际处理分离是一个常见的范例。 例如,如果你正在编写应该在一个文件夹中计算文件types的代码,那么可以写这个(特别是当他/她来自一个强制所有类的语言的时候):

 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); }); } } 

现在,如果你想testing,你需要改变你的代码,以注入一个假fs模块:

 class FileTypeCounter { constructor(fs) { this.fs = fs; } countFileTypes(dirname, callback) { this.fs.readdir(dirname, function (err) { // ... }); } } 

现在,每个使用你的类的人都需要在构造函数中注入fs 。 由于这是无聊的,一旦你有很长的依赖关系图,你的代码会变得更加复杂,开发者们发明了DI容器,他们可以在这里configuration东西,而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)); }); } 

现在你有两个超多function的开箱即用的function,一个是做IO,另一个是处理数据。 fileTypeCounter是一个纯粹的function,并且非常容易testing。 getAllFilesInDir是不纯的,但这是一个常见的任务,你经常会发现它已经在npm上 ,其他人已经为它写了集成testing。 getAllFileTypesInDir只是用一点控制stream来组合你的函数。 对于要确保整个应用程序正常工作的集成testing,这是一个典型情况。

通过在IO和数据处理之间分离你的代码,你根本不需要注入任何东西。 如果你不需要注入任何东西,这是一个好兆头。 纯函数是最容易testing的东西,而且仍然是项目之间共享代码的最简单方法。

过去,我们从Java和.NET中了解它们的DI容器并不存在。 随着节点6来到ES6代理开放这种容器的可能性 – 例如Awilix 。

所以让我们重写你的代码到现代ES6。

 class Team { constructor ({ User }) { this.User = user } getTeam (teamId) { return this.User.find({ teamId: teamId }) } } 

和testing:

 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 } from 'awilix' import Team from './Team' import User from './User' const container = createContainer() .registerClass({ Team, 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请求等待你的服务一次,等等。