许多Node.js开发人员使用require()来绑定模块(排他性地)使用模块的硬依赖关系来绑定模块,但是它们的优缺点还有其他方法。 我将在本文中讨论它们。 将考虑四种方法:
- 硬依赖性(require())
- 依赖注入
- 服务定位器
- 嵌入式依赖容器(DI容器)
关于模块的一点
模块和模块化体系结构是Node.js的基础。 模块提供封装(隐藏实现细节并仅打开带有module.exports的接口),代码重用,将逻辑代码拆分为文件。 几乎所有的Node.js应用程序都包含许多必须以某种方式进行交互的模块。 如果您错误地绑定了模块,或者甚至让模块之间的交互发生漂移,那么您很快就会发现应用程序开始“崩溃”:一个地方的代码更改导致另一个地方的崩溃,根本就不可能进行单元测试。 理想情况下,模块应具有较高的
连通性 ,但
耦合度应较低。
上瘾
当使用require()时,一个模块对另一个模块的依赖性很大。 这是一种有效,简单且通用的方法。 例如,我们只想连接负责与数据库交互的模块:
优点:
缺点:
- 重用该模块很困难(例如,如果我们要重复使用我们的模块,但是要使用数据库的另一个实例)
- 难以进行单元测试(您必须创建一个虚拟数据库实例并将其以某种方式传递给模块)
总结:
该方法适用于小型应用程序或原型,以及连接无状态模块(工厂,设计人员和功能集)的方法。
依赖注入
依赖项注入的主要思想是将依赖项从外部组件传输到模块。 因此,消除了模块中的硬依赖性,并且有可能在不同的上下文中(例如,在不同的数据库实例中)重用它。
可以通过在构造函数参数中传递依赖关系或通过设置模块属性来实现依赖关系注入,但实际上最好使用第一种方法。 让我们在实践中通过使用工厂创建数据库实例并将其传递到我们的模块来应用依赖项的实现:
外部模块:
const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance);
现在,我们不仅可以重用我们的模块,而且还可以轻松地为其编写单元测试:只需为数据库实例创建一个模拟对象并将其传递给模块即可。
优点:
- 易于编写单元测试
- 提高模块的可重用性
- 参与度降低,连接性增强
- 将创建依赖关系的职责转移到更高的级别-通常,这可以提高程序的可读性,因为重要的依赖关系集中在一个地方,而不会被模块分散
缺点:
- 需要进行更彻底的依赖关系设计:例如,必须遵循一定顺序的模块初始化
- 依赖管理的复杂性,尤其是当存在许多依赖关系时
- 模块代码可理解性的下降:当依赖关系来自外部时编写模块代码更加困难,因为我们无法直接查看此依赖关系。
总结:
依赖注入增加了应用程序的复杂性和大小,但作为回报,它允许重用并简化测试。 开发人员应确定在特定情况下对他而言更重要的事情-硬依赖性的简单性或引入依赖性的更广泛可能性。
服务定位器
这个想法是要有一个依赖项注册表,当加载任何模块的依赖项时,它可以充当中介。 代替硬绑定,模块从服务定位器请求依赖项。 显然,这些模块具有新的依赖关系-服务定位器本身。 服务定位器的一个示例是Node.js模块系统:模块使用require()请求依赖项。 在以下示例中,我们将创建一个服务定位器,在其中注册数据库实例和我们的模块。
外部模块:
const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule');
我们的模块:
应该注意,服务定位器存储服务工厂而不是实例,这是有道理的。 我们获得了延迟初始化的好处,现在我们不必担心模块的初始化顺序-所有模块将在需要时进行初始化。 另外,我们有机会将参数存储在服务定位器中(请参阅“ someParameter”)。
优点:
- 易于编写单元测试
- 重用模块比沉迷模块容易
- 与沉迷相比,参与度降低,连接性增强
- 将创建依赖关系的责任转移到更高的层次
- 无需遵循模块初始化顺序
缺点:
- 重用模块比实现依赖关系更困难(由于服务定位器的附加依赖关系)
- 可读性:更加难以理解服务定位器所需的依赖关系
- 与依赖注入相比,参与度更高
总结
通常,服务定位器类似于依赖项注入,在某些方面它更容易(没有初始化顺序),在某些方面它更困难(比重用代码的可能性小)。
嵌入式依赖容器(DI容器)
服务定位器有一个缺点,由于它在实践中很少应用-模块对定位器本身的依赖性。 嵌入式依赖项容器(DI容器)没有此缺点。 实际上,这是一个具有附加功能的服务定位器,该功能在创建模块实例之前确定模块的依赖关系。 您可以通过从模块构造函数中解析和提取参数来确定模块依赖性(在JavaScript中,您可以使用toString()将指向函数的链接转换为字符串)。 如果仅针对服务器进行开发,则此方法适用。 如果编写了客户端代码,则通常会使其缩小,并且提取参数名称将毫无意义。 在这种情况下,依赖项列表可以作为字符串数组传递(在Angular.js中,基于使用DI容器,将使用此方法)。 我们使用构造函数参数的解析来实现DI容器:
const fnArgs = require('parse-fn-args'); module.exports = function() { const dependencies = {}; const factories = {}; const diContainer = {}; diContainer.factory = (name, factory) => { factories[name] = factory; }; diContainer.register = (name, dep) => { dependencies[name] = dep; }; diContainer.get = (name) => { if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && diContainer.inject(factory); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } diContainer.inject = (factory) => { const args = fnArgs(factory) .map(dependency => diContainer.get(dependency)); return factory.apply(null, args); } return dependencies[name]; };
与服务定位器相比,已添加了inject方法,该方法在创建模块实例之前确定模块的依赖关系。 外部模块代码变化不大:
const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule');
我们的模块看起来与简单的依赖项注入完全相同:
现在,可以在一个DI容器的帮助下调用我们的模块,并使用简单的依赖注入将其直接传递给必要的依赖实例。
优点:
- 易于编写单元测试
- 轻松复用模块
- 减少参与,增加模块的连接性(尤其是与服务定位器相比)
- 将创建依赖关系的责任转移到更高的层次
- 无需跟踪模块初始化
最大减号:
总结
这种方法更难以理解,并且包含更多代码,但是由于其强大和优雅,值得花时间在上面。 在小型项目中,此方法可能是多余的,但如果正在设计大型应用程序,则应考虑使用此方法。
结论
考虑了Node.js中模块绑定的基本方法。 通常情况下,“银弹”并不存在,但是开发人员应意识到可能的替代方法,并针对每种特定情况选择最合适的解决方案。
本文基于2017年发布的
Node.js设计模式书中的一章。 不幸的是,本书中的许多内容已经过时了,因此我不能100%建议阅读,但今天有些内容仍然有意义。