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

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; 
Run Code Online (Sandbox Code Playgroud)

一个简单的测试如下所示:

 // 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);
      });
    });
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

测试:

// 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);
  });
});
Run Code Online (Sandbox Code Playgroud)

Joh*_*ald 6

关于您的问题:我认为JS社区中没有通用的做法。我见过在野外两种类型,需要修改(如联控proxyquire)和构造器注入(通常使用专用的DI容器)。但是,就我个人而言,我认为不使用DI容器更适合JS。这是因为JS是一种动态语言,具有一流的公民功能。让我解释一下:

使用DI容器对所有内容强制执行构造函数注入。由于两个主要原因,它造成了巨大的配置开销:

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

关于第一个参数:我不会只为单元测试调整代码。如果它使您的代码更简洁,更简单,更通用,更不易出错,那么请继续努力。但是,如果您唯一的原因是您的单元测试,那么我将不做权衡。您可以通过要求修改和猴子补丁来达到目标。而且,如果您发现自己编写了太多的模拟,则可能根本不应该编写单元测试,而应该编写集成测试。埃里克·埃利奥特(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);
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果要进行测试,则需要更改代码以注入假fs模块:

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

现在,正在使用您的类的每个人都需要注入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));
    });
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以立即使用两个超级通用功能,一个正在执行IO,另一个正在处理数据。fileTypeCounter是一种纯函数,非常易于测试。getAllFilesInDir这是不纯净的,但却是一项常见的任务,您经常会在npm上找到它,其他人为此编写了集成测试。getAllFileTypesInDir只需一点点控制流程即可构成您的功能。这是集成测试的典型情况,您需要确保整个应用程序正常运行。

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


Jef*_*eff 5

过去,我们从 Java 和 .NET 中了解到的 DI 容器并不存在。随着 Node 6 的出现,ES6 代理开启了这种容器的可能性——例如Awilix

因此,让我们将您的代码重写为现代 ES6。

class Team {
  constructor ({ User }) {
    this.User = user
  }

  getTeam (teamId) {
    return this.User.find({ teamId: teamId })
  }
}
Run Code Online (Sandbox Code Playgroud)

和测试:

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)
  })
})
Run Code Online (Sandbox Code Playgroud)

现在,使用 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()
Run Code Online (Sandbox Code Playgroud)

这很简单;Awilix 也可以处理对象生命周期,就像 .NET / Java 容器一样。这让你可以做一些很酷的事情,比如将当前用户注入你的服务,每个 http 请求初始化你的服务等。