dan*_*ael 4 .net javascript dependency-injection node.js dependency-inversion
我是来自.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);
  });
});
关于您的问题:我认为JS社区中没有通用的做法。我见过在野外两种类型,需要修改(如联控或proxyquire)和构造器注入(通常使用专用的DI容器)。但是,就我个人而言,我认为不使用DI容器更适合JS。这是因为JS是一种动态语言,具有一流的公民功能。让我解释一下:
使用DI容器对所有内容强制执行构造函数注入。由于两个主要原因,它造成了巨大的配置开销:
关于第一个参数:我不会只为单元测试调整代码。如果它使您的代码更简洁,更简单,更通用,更不易出错,那么请继续努力。但是,如果您唯一的原因是您的单元测试,那么我将不做权衡。您可以通过要求修改和猴子补丁来达到目标。而且,如果您发现自己编写了太多的模拟,则可能根本不应该编写单元测试,而应该编写集成测试。埃里克·埃利奥特(Eric Elliott)就此问题写了一篇很棒的文章。
关于第二个参数:这是一个有效的参数。如果要创建一个只关心接口而不关心实际实现的组件,我将选择简单的构造函数注入。但是,由于JS不会强制您对所有内容使用类,所以为什么不仅使用函数呢?
在函数式编程中,将状态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是一种纯函数,非常易于测试。getAllFilesInDir这是不纯净的,但却是一项常见的任务,您经常会在npm上找到它,其他人为此编写了集成测试。getAllFileTypesInDir只需一点点控制流程即可构成您的功能。这是集成测试的典型情况,您需要确保整个应用程序正常运行。
通过将IO和数据处理之间的代码分开,您根本不需要注入任何东西。而且,如果您不需要注射任何东西,那是一个好兆头。纯函数是最容易测试的东西,仍然是在项目之间共享代码的最简单方法。
过去,我们从 Java 和 .NET 中了解到的 DI 容器并不存在。随着 Node 6 的出现,ES6 代理开启了这种容器的可能性——例如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 请求初始化你的服务等。