11月阅读周·编写可测试的JavaScript代码:复杂度之依赖注入篇

举报
叶一一 发表于 2024/11/20 10:14:49 2024/11/20
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》

当前阅读周书籍编写可测试的JavaScript代码

依赖注入

依赖会让代码变得复杂,也会让构建、测试、调试变得更加困难,几乎所有我们希望变简单的事情,依赖都会使其变得困难。随着应用程序的增长,管理代码依赖的时间越来越长。

注入和模拟是松散的关系。注入负责构造对象,并将对象注入到代码中;而模拟是在调用的时候替换对象或方法以便于测试。在测试的时候,可以(也应该)使用注入工具向代码中插入mock版本的对象,但在生产环境代码中不应该使用模拟框架!

工厂化依赖,或手动将依赖注入到构造函数或方法调用中,有助于减少代码的复杂性,但也会增加一些开销:如果一个对象的依赖项需要注入,而另外一个对象此时则负责构建该对象。我们就把问题“皮球”踢到另外一层吗?是的,就是这样的!“皮球”必须止于某个地方,而这个地方通常是应用程序或测试的开始。对象构造的映射是动态定义到前端的,这允许任何对象都可以很容易地从一个交换到另一个。不管是测试对象还是升级对象,这都是一个非常不错的方法。

依赖注入器可以为代码构建和注入完全成型的对象。当然,注入器实际上必须被告知如何构建这些对象,接着,当需要该对象的实例时,注入器就提供一个实例。没有什么“魔法”(也许有点儿),注入器只能构建特定的对象。注入器需要接受很多不同的方式来描述如何构造需要的对象,但不会迷失方向:告知注入器如何构建一个对象(或注入器通过控制代码构建对象)。

还有一个和注入器构建对象相关的是:作用域。作用域通知注入器是创建一个新实例还是重用现有实例。告诉注入器每个对象的作用域,当代码要求该类型的一个对象时,注入器就会做相应的事情(创建一个新实例,或者重用现有实例)。

我们知道,在构造函数里(或对象的其他地方)实例化依赖对象是一个紧耦合依赖,会让测试工作变得更加复杂——所以让注入器来做这个工作。

让我们简要地看一下knit(https://github.com/nicocube/knit),Google Guice式的JavaScript注入器。我们知道这段代码“不太好”:

var spaceShuttle = function () {
  this.mainEnginew = new spaceShuttleMainEnginengine();
  this.boostErengine1 = new spaceShuttleSolidRocketBooster();
  this.boostErengine2 = new spaceShuttleSolidRocketBooster();
  this.arm = new shuttlEremoteManiPulatorSystem();
};

所有这些对象的实例化都是在构造函数中,那么没有真正的引擎和助推器,我们要如何测试航天飞机(SpaceShuttle)呢?第一步,让构造函数可注入:

var spaceShuttle = function (mainEngine, b1, b2, arm) {
  this.mainEngine = mainEngine;
  this.boosterEngine1 = b1;
  this.boosterEngine2 = b2;
  this.arm = arm;
};

现在,我们可以使用knit定义希望构建的对象了:

knit = require('knit');
knit.config(function (bind) {
  bind('MainEngine').to(SpaceShuttleMainEngine).is('construtor');
  bind('BoostErengine1').to(SpaceShuttleSolidRocketBooster).is('constructor');
  bind('BoostErengine2').to(SpaceShuttleSolidRocketBooster).is('constructor');
  bind('Arm').to(ShuttlEremoteManiPulatorSystem).is('constructor');
  bind('ShuttleDiscovery').to(SpaceShuttle).is('constructor');
  bind('ShuttleEneavor').to(SpaceShuttle).is('constructor');
  bind('Pad').to(new LaunchPad()).is('singleton');
});

这里的SpaceShuttleMainEngine、SpaceShuttleSolidRocketBooster以及Shuttle RemoteManipulatorSystem对象是在其他地方定义的,示例如下:

var SpaceShuttleMainEngine = function() {
  ...
}

现在每当请求MainEngine时,knit将会把该对象实例填充到:

var SpaceShuttle = function (MainEngine, boosterEngine1, boosterEngine2, Arm) {
  this.mainEngine = MainEngine;
  ...
};

所以,knit.inject方法里的整个SpaceShuttle对象和其所有的依赖项对象都可用了,示例如下:

knit.inject(function (ShuttleDiscovery, ShuttleEndeavor, Pad) {
  ShuttleDiscovery.blastOff(Pad);
  ShuttleEndeavor.blastOff(Pad);
});

knit会递归查找SpaceShuttle的所有依赖项,并为我们构建SpaceShuttle对象。指定Pad作为单例,以确保任何对Pad对象的请求都将返回这个单一实例。

假设随着时间的流逝,墨西哥创建一个比加拿大还惊奇的ShuttleRemoteManipulator System。只需要将原来的Arm绑定切换成新的ShuttleRemoteManipulatorSystem即可:

bind('Arm').to(MexicanShuttlEremoteManiPulatorSystem).is('constructor');

现在,不需要改变其他任何代码,所有要求得到arm的对象都会获取墨西哥版本的Arm,而不是加拿大版本的Arm。

除了交换新旧版本和不同版本的对象,通过修改绑定,注入框架还可以向应用程序注入模拟或测试对象。

测试航天飞机发射而又不实际发射是一个很好的选择,可以通过简单修改注入器的绑定即可。

AngularJS框架也通过正则表达式使用了大量的依赖注入。除了缓解测试,控制器以及其他一些功能(通过函数参数列表)可以指定哪些对象需要做哪些工作,接着相对应的对象将被注入。

总结

依赖注入真正促进了更大型应用程序的增长,所有重要的应用程序都将增长,在一开始就使用依赖注入,将会是一个很好的开端。


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。