10月阅读周·编写可测试的JavaScript代码:复杂度之圈复杂度篇

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

背景

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

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

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

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

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

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

圈复杂度

圈复杂度是表示代码中独立现行路径的数量。换句话说,它是为锤炼所有的代码,需编写的单元测试的最小数量。让我们来看一个示例:

function sum(a, b) {
  if (typeof a !== typeof b) {
    throw new error('cannot sum different types!');
  } else {
    return a + b;
  }
}

该方法的圈复杂度是2。也就是说,要测试每个分支,并获得100%的代码覆盖率,要编写两个单元测试。

生成代码的圈复杂度可以使用像jsmeter这样简单的命令行工具。然而,判断什么才是最优的圈复杂度并不那么简单。例如,Thomas J. McCabe在他的论文A Complexity Measure中推测:任何方法的圈复杂度都不应该大于10。与此同时,一项研究表明,圈复杂度超过25,才会和Bug错误有所关联。那么为什么要尽量保持圈复杂度值小于10呢?10这个数字不是臆想出来的,而是一个合理的数字,所以当圈复杂度达到25的时候,代码和Bug错误之间才有关联,为了实现合理性和可维护性,保持较低的圈复杂度是一个好办法。澄清一下,代码的圈复杂度值为25,表示其非常复杂,不管该代码当前的Bug数量是多少,在圈复杂度如此高的代码方法里修改代码,产生新Bug几乎是不可避免的。如表2-1,Aivosto.com网站给出了随着圈复杂度的增加“错误修复”出现的几率。

表1-1 圈复杂度和错误修复概率

圈复杂度

错误修复概率

1-10

5%

20-30

20%

>50

40%

高达100

60%

可以从该表中看到一个很有趣的地方:修复相对简单的代码,有5%的几率引入新Bug。这很重要!圈复杂度为20以上时,“错误修复”出现的几率增长了4倍。我从来没有见过一个函数的圈复杂度达到50,更别说是100了。如果我们看到代码的圈复杂度接近这些数字,应该避而远之。

另外,为圈复杂度如此高的代码编写大量单元测试的做法是不可取的。McCabe在上述文章中指出,复杂度超过16的函数也是最不可靠的。

阅读别人的代码是确保代码质量和正确性的最可靠的指标(稍后详细介绍)。阅读代码时,读者必须跟踪所有的分支,以便了解这些代码都在做什么。最近,许多科学家进行了短期记忆研究,其中最著名的是:人类的短期记忆只能记住7±2个条目(又名Miller定律)

圈复杂度高的代码通常是由很多if/then/else语句(或switch语句,不用switch是因为它不是语言精髓,对吧?)造成的。最简单的重构修复,是将方法分解成更小的方法,或使用一个lookup表。如下是我们所说的场景示例:

function doSomething(a) {
  if (a === 'x') {
    dox();
  } else if (a === 'y') {
    doy();
  } else {
    doz();
  }
}

使用lookup表重构的示例如下:

function doSomething(a) {
  var lookup = { x: dox, y: doy },
    def = doz;
  lookup[a] ? lookup[a]() : def();
}

注意,通过使用lookup表重构条件语句以后,我们并没有减少必需的单元测试数量。对于圈复杂度高的方法,可以将其分解成多个更小的方法。现在可以针对每个小函数编写很多很小的单元测试,而不必再为一个单独的函数编写大量的单元测试了,这样函数代码将更具有可维护性。

在代码提交之前或在构建过程中,添加圈复杂度检查是很容易的。只是要确保真的会查看输出结果,并相应地采取行动!对于当前项目,首先要分解最复杂的方法和对象,但在编写单元测试之前不要这样做!当然,重构的首要原则是“无害”,除非让前后两个版本的代码经过相同的单元测试验证,否则我们不能确保是否遵守了这个原则。

jscheckstyle(是一个非常方便的计算圈复杂度的工具,它可以计算每个函数和方法的圈复杂度。在第8章,我们将把jscheckstyle与Jenkins工具集成在一起,对圈复杂度过高且可以重构的代码进行标记,但现在,让我们先大致了解一下。

总结

jscheckstyle等实用工具在源代码层级提供了一个大致的预览。虽然没有绝对的编码实践和风格,但圈复杂度已经被证明是一个有效衡量代码复杂性的方法,所以要深刻理解它的含义。


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

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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