Javascript 节点中的依赖注入和 DIP:需要与构造函数注入

Javascript dependency injection & DIP in node: require vs constructor injection

我是 .NET 世界的 NodeJs 开发新手 我正在网上搜索在 Javascript

中重新分级 DI / DIP 的最佳实践

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

对我来说,当我使用 require 时,我似乎耦合到特定文件,而使用构造函数来接收我的依赖项更灵活。

在 javascript 中,您会推荐什么作为最佳实践?(我正在寻找架构模式而不是 IOC 技术解决方案)

在网上搜索我发现了这个博客 post(评论中有一些非常有趣的讨论): https://blog.risingstack.com/dependency-injection-in-node-js/

它很好地总结了我的冲突。 这是博客 post 中的一些代码,可以让您理解我在说什么:

// 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 社区中没有通用的做法。我在野外见过这两种类型,需要修改(如 rewire or proxyquire) and constructor injection (often using a dedicated DI container). However, personally, I think not using a DI container is a better fit with JS. And that's because JS is a dynamic language with functions as first-class citizens。让我解释一下:

使用 DI 容器强制构造函数注入一切。由于两个主要原因,它会产生巨大的配置开销:

  1. 在单元测试中提供模拟
  2. 创建对其环境一无所知的抽象组件

关于第一个参数:我不会仅仅为了我的单元测试而调整我的代码。如果它让你的代码更干净、更简单、更通用并且更不容易出错,那就出去吧。但如果你的唯一原因是你的单元测试,我不会采取权衡。你可以通过 require modifications 和 monkey patching. And if you find yourself writing too many mocks, you should probably not write a unit test at all, but an integration test. Eric Elliott has written a great article 解决这个问题。

关于第二个参数:这是一个有效的参数。如果你想创建一个只关心接口而不关心实际实现的组件,我会选择简单的构造函数注入。但是,既然 JS 不强制你对所有事情都使用 classes,为什么不直接使用函数呢?

functional programming中,将有状态 IO 与实际处理分开是一种常见的范例。例如,如果您正在编写应该对文件夹中的文件类型进行计数的代码,那么可以这样写(尤其是当 he/she 来自一种在任何地方都强制执行 classes 的语言时):

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

现在,每个使用您的 class 的人都需要将 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,另一个用于处理数据。 fileTypeCounterpure function and super-easy to test. getAllFilesInDir is impure but a such a common task, you'll often find it already on 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 请求实例化您的服务一次,等等。