【问题标题】:Javascript dependency injection & DIP in node: require vs constructor injection节点中的 Javascript 依赖注入和 DIP:需要与构造函数注入
【发布时间】:2016-10-16 15:35:54
【问题描述】:

我是来自 .NET 世界的 NodeJs 开发新手 我正在网上搜索在 Javascript 中重新分级 DI / DIP 的最佳实践

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

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

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

在网上搜索时,我看到了这篇博文(在 cmets 中有一些非常有趣的讨论): 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);
  });
});

【问题讨论】:

    标签: javascript .net node.js dependency-injection dependency-inversion


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

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

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

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

      关于第一个参数:我不会仅仅为了我的单元测试而调整我的代码。如果它使您的代码更简洁、更简单、更通用且更不容易出错,那么就去吧。但是,如果您唯一的原因是您的单元测试,我不会进行权衡。你可以通过需要修改和monkey patching 走得很远。如果你发现自己写了太多的模拟,你可能根本不应该写一个单元测试,而是一个集成测试。 Eric Elliott 写了a great article 关于这个问题。

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

      functional programming 中,将有状态 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,另一个用于处理数据。 fileTypeCounterpure function 并且超级容易测试。 getAllFilesInDir 是不纯的,但却是一项常见的任务,您经常会在 npm 上找到它,其他人已经为它编写了集成测试。 getAllFileTypesInDir 只是用一点控制流来组合你的函数。这是您希望确保整个应用程序正常工作的集成测试的典型案例。

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

      【讨论】:

      • 感谢您的详细解答!我将不得不更详细地研究函数式编程,看起来 javascript 非常适合它。您有任何示例存储库可以查看吗?
      • This one 是我所知道的 JavaScript 函数式编程最全面的介绍。
      猜你喜欢
      • 2011-02-02
      • 1970-01-01
      • 1970-01-01
      • 2021-05-13
      • 1970-01-01
      • 1970-01-01
      • 2019-04-20
      • 2018-06-17
      相关资源
      最近更新 更多