Javascript依赖注入&;浸入节点:需要vs构造函数注入

Javascript依赖注入&;浸入节点:需要vs构造函数注入,javascript,.net,node.js,dependency-injection,dependency-inversion,Javascript,.net,Node.js,Dependency Injection,Dependency Inversion,我是来自.NET世界的NodeJs开发新手 我在网上搜索Javascript中重新分级DI/DIP的最佳实践 在.NET中,我会在构造函数中声明依赖项,而在javascript中,我看到一种常见的模式是通过require语句在模块级声明依赖项 对我来说,当我使用require时,我会耦合到一个特定的文件,而使用构造函数来接收依赖项则更灵活 作为javascript的最佳实践,您建议做什么?(我在寻找架构模式,而不是IOC技术解决方案) 在网上搜索时,我发现了这篇博文(在评论中有一些非常有趣的讨论

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

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

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

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

在网上搜索时,我发现了这篇博文(在评论中有一些非常有趣的讨论):

它很好地概括了我的冲突。 以下是博客文章中的一些代码,让您理解我所说的:

// 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);
}
测试:


关于你的问题:我不认为JS社区有一个普遍的做法。我在野外见过这两种类型,它们都需要修改(如or)和构造函数注入(通常使用专用DI容器)。然而,就个人而言,我认为不使用DI容器更适合JS。这是因为JS是一种具有。让我解释一下:

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

  • 在单元测试中提供模拟
  • 创建对其环境一无所知的抽象组件
  • 关于第一个参数:我不会仅仅为了单元测试而调整代码。如果它能让你的代码更干净、更简单、更通用、更不容易出错,那就试试吧。但是,如果您的唯一原因是单元测试,那么我不会进行权衡。您可以在需要修改和修改的情况下走得很远。如果您发现自己编写了太多的模拟,那么您可能根本不应该编写单元测试,而应该编写集成测试。Eric Elliott写过关于这个问题的文章

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

    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与实际处理分离是一种常见的范例。例如,如果您正在编写用于计算文件夹中的文件类型的代码,则可以编写以下代码(尤其是当他/她来自一种到处强制类的语言时):

    现在,如果您想测试这一点,您需要更改代码,以便注入一个假的
    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
    是一个非常简单的测试工具
    GetAllFileIndir
    是一项不纯净的任务,但这是一项如此常见的任务,您经常会发现它已经在其他人为它编写集成测试的地方出现了
    getAllFileTypesInDir
    只是通过一点控制流来组合函数。这是集成测试的典型案例,您需要确保整个应用程序正常工作


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

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

    因此,让我们将您的代码重写为现代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,让我们编写我们的合成根:


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

    谢谢您的详细回答!我必须更详细地研究函数式编程,看起来javascript非常适合它。你有什么可以看的示例库吗?这是我所知道的关于JavaScript函数式编程的最全面的介绍。
    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)
      })
    })
    
    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()