4

我是来自 .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);
      });
    });

与迪:

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

2 回答 2

8

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

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

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

关于第一个论点:我不会只为我的单元测试调整我的代码。如果它使您的代码更简洁、更简单、更通用且更不容易出错,那么就去吧。但是,如果您唯一的原因是您的单元测试,我不会进行权衡。你可以通过 require modify 和monkey patching 走得很远。如果你发现自己写了太多的模拟,你可能根本不应该写一个单元测试,而是一个集成测试。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 和数据处理之间分离代码,您根本不需要注入任何东西。如果你不需要注射任何东西,这是一个好兆头。纯函数是最容易测试的东西,并且仍然是在项目之间共享代码的最简单方法。

于 2016-06-17T10:36:58.760 回答
5

过去,我们从 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 请求实例化你的服务,等等。

于 2016-09-20T06:24:46.220 回答