11月阅读周·编写可测试的JavaScript代码:复杂度之依赖注入篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)