10月阅读周·编写可测试的JavaScript代码:复杂度之扇出篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
扇出
扇出(Fan-out)测量函数直接或间接依赖的模块或对象的数量。扇出和扇入(Fan-in)理论是由Sallie Henry和Dennis Kafura在他们编写的Software Structure Metrics Based on Information Flow中首次提出的,文章在1981年发表于IEEE Transactions on Software Engineering。他们推测,软件成本的50%到75%是维护成本,早在设计阶段他们就想测量软件的复杂性。以前的工作经验表明,高复杂性会导致软件质量较低,他们认为,如果软件在构建的时候,其复杂性可以测量和控制的话,那就是双赢。他们很熟悉McCabe的一些作品,讲述测量圈复杂度及其与软件质量之间的关系(出版于1976年,本章前面提到过),以及其他词法分析技术,但他们相信,通过测量代码的底层结构,而不仅仅是计算令牌,可以得到更准确的复杂性测量结果。
所以,在函数和模块之间利用信息理论和分析流,他们炮制了复杂度的测量公式:
(fan_in * fan_out)²
然后在Unix操作系统上计算该值(或其他变体)后,发现该值和“软件变更”(例如Bug修复)有98%的相关性。即:用该公式计算后,越复杂的函数或模块,该函数或模块就越有可能发生Bug。
所以,扇入和扇出到底是什么?如下是扇出的定义:
过程A的扇出是表示过程A的内部流程数量与过程A所更新的数据结构数量之和。
在该定义中,如下任意操作都算作一个内部流程(以方法B和C为例):
1.如果A调用B;
2.如果B调用A,并且A返回一个B随后可以利用的值;
3.如果C调用A和B,且A的返回值传递给B。
所以,将函数A所有的内部流程,加上A所更新的全局结构(相对于A外部),产生的数字就是函数A的扇出。扇入的定义类似,稍后将会看到。
对于所有的函数,计算该扇出值和该值所对应的扇入值,将两数相乘,并进行平方计算,其结果数字就是一个函数的复杂度。
使用该公式,对于高度复杂的代码,Henry和Kafura注意到了3个问题。首先,高扇入和扇出的代码,可能表示一个函数正在尝试做太多事情,应该避免;第二,高扇入和扇出,可以判定出系统的压力点(stress points),维护这些函数将会非常困难,因为它们关联太多的系统其他部分;第三,它们不够精细(inadequate refinement),这意味着函数需要重构,因为它太大且要做的事情太多,或者缺少抽象层才导致该函数的高扇入和高扇出。
基于函数复杂度测量,Henry和Kafura注意到,一旦生成模块复杂度值,从中就可以测量多个模块本身之间的耦合情况。在函数级别上应用这些经验,可以确定哪些模块具有较高的复杂度,并且需要被重构,或者表明需要一个抽象层。他们还发现,不管模块有多大,一般绝大多数模块的复杂度都取决于一小部分的函数(一般是3个!)。所以谨防“mongo”式函数,它做的事情太多,而大多数其他函数能做的却很少!
Henry和Kafura还研究了函数长度(代码行数),并且发现,只有28%的20行以下的函数才会发生个别错误,而78%的超过20行的函数会有很多错误。代码要保持短小简洁啊!
直觉上,扇出越大的函数问题越多。JavaScript很容易声明并使用全局变量,但标准的JavaScript告诉我们不要使用全局空间,而要使用局部命名空间。这有助于减少扇出定义中的一部分扇出(过程A更新的数据结构数量),但我们仍然必须保持函数的内部流程。
检查这个非常容易,可以通过查看函数所需的外部对象数量来检查。下面的例子使用了YUI的异步请求机制,引入myModule需要的JavaScript代码。这里要理解的要点是,告知YUI我们的代码所需要的依赖,让YUI帮我们获取这些依赖。依赖加载后,将执行我们的回调函数:
YUI.use(
'myModule',
function (y) {
var myModule = function () {
this.a = new y.a();
this.b = new y.b();
this.c = new y.c();
};
Y.myModule = myModule;
},
{ requires: ['a', 'b', 'c'] },
);
myModule构造函数的扇出是3(在这种情况下,对象甚至都没有被用过,它们只是被创建并且存储了未来要用的方法,但代码还是要再次编写的,所以它们不利于构造函数的扇出)。该模块的构造函数没有实例化且未被使用。3是一个不错的数字,但是,一般外部对象实例化myModule,并使用内部方法调用返回值,这一数字将会增加。扇出用于衡量在我们脑海中编辑此方法的跟踪路径。它是外部方法的数量和该方法所操作对象的数量之和。
不管我们是否计算内部流程,Miller定律认为,试图记住并跟踪超过七件事会非常困难。在后来的再分析中,这一数字降至4。问题在于没有哪个具体的数字是一个危险的指标,但当扇出大于7(甚至4)时,就要看看是怎么回事了,我们可能需要重构。过度的扇出会掩盖其他问题,即紧密耦合。
总结
所有的通用模拟(mock)都消失了,测试代码就完全控制了传入的模拟对象,可以让代码进行更广泛的测试以及具有更大的灵活性。
在注入和创建外观时,可以博弈一下将对象实例化后可以推到多远。继续这样操作,可以直接进入基于事件的超级松耦合架构。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)