编码概念!圈复杂度
圈式什么?连拼写检查都识别不了这个词,但它是一个非常有用的软件指标,可以帮助你理解软件的工作原理。
几年前读到过关于它的文章,现在看来它的使用率似乎已经下降了。我觉得它是开发人员工具库中非常有价值的工具,应该在代码审查和代码库的可维护性方面使用。我们都知道要保持代码“简洁”。我们都听说过KISS原则,但我们是否曾被告知过什么是真正的简洁,以及应该如何衡量它?
嗯,这就是圈复杂度进入框架的地方。
定义
摘自维基百科:
圈复杂度 是一种 软件度量,用于指示程序的复杂度。它定量衡量程序源代码中线性独立路径的数量 。它由Thomas J. McCabe, Sr.于 1976 年开发。 圈复杂度是使用程序的控制流图 计算的 :图 的节点 对应于程序中不可分割的命令组,如果第二个命令可能在第一个命令之后立即执行,则 有向 边连接两个节点。圈复杂度也可以应用于程序中的各个 函数、 模块、 方法 或 类 。
这实际上意味着什么
本质上,它是通过一段逻辑的不同路线数量。换句话说,它通常在可维护性指数的背景下考虑。特定函数中的分支越多,维护其操作的心理模型就越困难。该指标大致相当于1 加上循环和 if 语句的数量。这很好地说明了值的配置方式,是的,x 可能大于 100 并直接移动,并且该路径的复杂度为 1,但是代码块/方法本身的分数为 11。
我为什么要关心?
代码覆盖率正成为开发周期中不可或缺的一部分。圈复杂度最终会影响您需要为给定代码段编写的不同单元测试的数量。方法中额外的路径需要编写额外的测试,并且代码中可能出现更多错误或出现错误的地方。
综合考虑以上所有因素,代码的圈复杂度最终决定了它的有效性、简洁性、可维护性和通用性。所以,这很重要,不是吗?
高“复杂性”可以直接转化为低可读性,这也意味着新开发人员更难理解正在发生的事情。
我确信您曾有过这样的经历:查看一些代码时,您不知道发生了什么,也不知道为什么要以“那样”的方式写。
所以下次你写东西的时候,记住,下一个看到它的人可能不是你。把它写得让你看到它时感到满意。这种方法在我完成一个新功能时总是很有帮助。
圈复杂度有多重要?
谁不喜欢一张好的表格呢?这张表格展示了一种方法可以具有的不同值以及这些值意味着什么?
复杂 | 这意味着什么 |
---|---|
1-10 | 结构化且编写良好的代码,易于测试。 |
10-20 | 相当复杂的代码可能难以测试。根据你的具体操作,如果出于合理的考虑,这些值仍然是可以接受的。 |
20-40 | 代码非常复杂,难以测试。您应该考虑重构,将其分解为更小的方法,或者使用设计模式。 |
>40 | 疯狂的代码,根本无法测试,几乎无法维护或扩展。这里面肯定有问题,需要进一步检查。 |
这些不同的层次有助于我们更好地理解正在编写的代码,以及这些代码对我们可能需要的任何测试资源的影响。它还让我们意识到,任何高复杂度的代码都会在未来给我们带来问题,我们应该在下一个可用的机会上花时间进行重构。
我们可以做什么来解决这个问题?
上表列出了不同的复杂程度,以及何时应该开始考虑重构代码。我们将介绍几种实现重构的方法。目前为止,最简单的方法是删除所有不需要的if或else语句。这些语句有时会在开发过程中出现,但不会被删除。你可能在代码库中发现一个常见的例子,如下所示。
var msg = "";
if (month == 12 && day == 25) { // + 2
msg = "Merry Christmas"; // +1
} else {
msg = "Have a nice day"; // +1
}
return msg; // +1 - Total 5
上面的代码看起来没什么问题。但是,如果我们简单地删除 else 语句,并将默认消息移到声明部分,就能立即减少 1 个复杂度点。这是一个简单且常见的更改。
造成高复杂性的另一个主要原因是 case 或 switch 语句。
switch (day) { // +1
case 0: return "Sunday"; // +2
case 1: return "Monday"; // +2
case 2: return "Tuesday"; // +2
case 3: return "Wednesday"; // +2
case 4: return "Thursday"; // +2
case 5: return "Friday"; // +2
case 6: return "Saturday"; // +2
default: throw new Exception(); // +2 Total 17!
}
在某些情况下,你无法摆脱像上面那样的代码块,这就是它们被设计的目的。但有时 switch 语句只是糟糕的代码设计。如果你的 switch 语句可能会增加,那么策略模式是一个不错的选择。在上面的例子中,我们不太可能在日历中添加新的日期,但举个例子:
switch (carGarage) {
case 'seat': return contactSeat(); // +2
case 'audi': return contactAudi(); // +2
default: return contactFord(); // +2 - Total 6
}
这里有 3 个 case 语句,但考虑到它当前的实现,我们预计它们会扩展。添加额外的 case 语句是扩展此代码的一种可能解决方案,但每个额外的 case 语句都会增加复杂性!策略模式可以很好地解决这个问题。
enum CarDealerTypes { Seat, Audi, Ford }
interface CarDealerStrategy {
CallDealer();
}
class SeatDealer implements CarDealerStrategy {
CallDealer() {
CallSeat(); // +1
}
}
class AudiDealer implements CarDealerStrategy {
CallDealer() {
CallAudi(); // +1
}
}
class FordDealer implements CarDealerStrategy {
CallDealer() {
CallFord(); // +1
}
}
class Dealership {
// Here is our alternative to the case statements, easy right!
CallDealer(dealer: CarDealerStrategy) {
dealer.CallDealer(); // +1
}
// These are the methods that will ultimately be used
ContactAudiDelership() {
this.CallDealer(new AudiDealer()); // +1
}
}
它的设置成本较高,而且一开始会稍微复杂一些。但是,添加 15 个 switch 语句后,您会很高兴自己决定 切换 方法!此外,我们将case 语句的复杂度从原来的 3 个降低到了策略模式的1 个。想象一下,如果您的 switch 语句执行了额外的逻辑,并且嵌入了额外的 if 语句,您会发现测试起来会非常困难!
用那个脑袋!
与所有开发相关的事物一样,要记住的最重要的事情是,你不能仅仅因为自己想要就改变一些东西。
重构和改进代码库对于保持简洁明了的环境至关重要。如果您发现代码运行顺畅,没有给您或您的客户带来任何问题,那么请不要因为代码指标显示错误而进行更改。
代码从编写的那一刻起就成了遗留问题,所以你的重构工作在下一轮开发中可能会被淘汰。如果代码正在被修改,就应该改进它。优秀的程序员应该修复他们在开发故事或功能时发现的任何问题,但不会修改那些需要额外测试且不会直接影响当前工作的代码。
工具
所以你理解了这个概念,也知道如何修复它,但是找出潜在问题最简单的方法是什么呢?大多数 IDE 都应该提供一些内置工具来帮助你。我现在就介绍几个:
Visual Studio
只需前往“分析 | 计算解决方案的代码指标”即可计算代码指标。更多详情请访问:Visual Studio - 代码指标帮助
VsCode
我最近用的一个很棒的扩展程序链接了进来,它会在函数顶部显示复杂度!可以在这里找到它:CodeMetric 扩展程序
大多数 IDE 都有相应的工具,因此您可以找到适合您的工具!
我希望这篇关于圈复杂度 的介绍能给你一些思考,并在将来的某个时候对你有所帮助。下面的附加阅读材料将更深入地探讨这个主题,如果你对此主题感兴趣,欢迎随时阅读。和往常一样,请在下方评论区告诉我们你的想法。
这篇文章最初发表在我自己的博客上:Design Puddle Blog - Coding Concepts- Cyclomatic Complexity
补充阅读
McCabes 完整论文: http://mccabe.com/pdf/mccabe-nist235r.pdf
从不同的角度看,为什么不应该使用它? https://www.cqse.eu/en/blog/mccabe-cyclomatic-complexity/
还有一些说明: https://dzone.com/articles/what-exactly-is-mccabe-cyclomatic-complexity
文章来源:https://dev.to/designpuddle/coding-concepts---cyclomatic-complexity-3blk