提到“质量”二字时,我们的第一反应往往是“有多少BUG?”“性能好不好?“这样的问题。我们对软件产品或服务的质量定义看其能不能满足用户的需求,包括功能、性能和体验等维度的指标,我们可以通过各种类型的检测手段来给出其质量高低的度量。但是,如果直接拿出一段源代码放在我们面前,问这段代码的质量好坏时,我们又该如何作答呢?
有人说:“好的代码就像好的笑话一样,它不需要解释(Good code is like a good joke: It needs no explanation)”。有编码经验的人对代码都有一定的“鉴赏力”,能凭感觉给出代码好坏的主观评价,看到所谓的“意大利面条式代码”都会感到不舒服,但是这样凭感觉的方式太个性化、太随意了,有没有一种公认的标准来鉴定代码质量呢?
Bob大叔在其著作《代码整洁之道》的前言中引用了这样一幅漫画:
图1代码质量的唯一有效度量指标
使用漫画中的“每分钟爆粗数量”来衡量代码质量是个很有趣的玩笑,强调了代码的可读易懂等这样的“内在”质量属性。相对于满足需求规范这样的“外在”质量属性,“内在”的代码质量属性强调的是支持实现功能需求的代码内部结构的质量。《Sonar code quality testing essential》一书中从七个维度定义了代码的这种内在质量,Sonar开发团队上纲上线的戏称为开发人员七宗罪:
- 编码规范:是否遵守了编码规范,遵循了最佳实践。
- 潜在的BUG:可能在最坏情况下出现问题的代码,以及存在安全漏洞的代码。
- 文档和注释:过少(缺少必要信息)、过多(没有信息量)、过时的文档或注释。
- 重复代码:违反了Don’tRepeat Yourself原则。
- 复杂度:代码结构太复杂(如圈复杂度高),难以理解、测试和维护。
- 测试覆盖率:编写单元测试,特别是针对复杂代码的测试覆盖是否足够。
- 设计与架构:是否高内聚、低耦合,依赖最少。
Martin Fowler在其著作《重构:改善即有代码的设计》中生动形象的使用“代码坏味道(Bad Code Smells)”来比喻低质量的代码设计和实现所显现的“症状”。书中罗列了22种代码坏味道以及对应的重构手法。
参照这些资料,现在我们可以用可测性,可读性,可理解性,容变性等代码可维护性维度的质量属性来衡量代码质量。代码质量指的是代码内在的非功能性的质量,用户不能直接体验到这种质量的好坏,代码质量不好,最直接的“受害者”是开发者或组织自身,因为代码质量好坏直接决定了软件的可维护性成本的高低,例如重复代码会造成维护成本的成倍增加;不规范的代码、不良注释和复杂度过高的代码会增加阅读和理解代码的难度,复杂度过高也会极大增加测试覆盖的难度,耗费过多人力,而缺少测试覆盖的代码会使得定位问题和修复问题的难度加大;结构不良、低内聚高耦合的代码则会使得哪怕是微小的需求变更或功能扩展都无从下手,修改的代价很可能超过了重写的代价。
至此,我们得到了一些定性的办法来衡量代码的质量,我们可以借助一些代码扫描工具来暴露代码的质量问题,也有了相应的重构方法和技巧来应对这些问题。但是,我们还是难以回答某段代码有多好或多差,两段代码相比哪个更好这样的问题,因为我们仍然没有完全解决代码质量的量化问题:同样都是代码质量问题,重复代码和过多注释的危害肯定是不一样的;同样都是方法太复杂,圈复杂度为10的方法和圈复杂度为20的方法相比,危害和修改难度也差别很大。所以我们不能直接用问题的数量来衡量质量,需要找到更精细合理的量化度量方法。
SQALE方法的质量模型
如何评估软件产品源代码质量一直是业界的一大挑战,SQALE(Software Quality Assessment based on Lifecycle Expectations)方法的出现提供一套科学的度量和分析方法,有效应对了这一挑战。SQALE方法整合了ISO-25010标准与代码规范,其目标是:以客观、准确、可复制和自动化的方式为评估软件应用程序的源代码提供支持;为管理技术债务提供一种有效的方法。SQALE是目前众多主流代码分析工具的参照标准,包括我们熟知的SonarQube,和CoderGears, SQUORE等商用代码扫描分析工具。
下面我们简单介绍一下SQALE方法的原理。SQALE方法包含两种模型:质量模型和分析模型。下图的树型结构展示了SQALE方法的质量模型:树根节点代表软件质量(此处即代码质量),从左向右展开,第一级定义了代码质量的特征分类,往下是每种特征的子类,最后是每个子类对应的属性/具体的度量项。
图2SQALE方法示意图(质量模型)
从左向右的方向是把代码质量不断细化分解为更小的单元,直到最小粒度可以直接度量的属性;从右向左的方向是把度量值逐步汇总到根节点,最终得到一个总的代码质量的度量值。表1是SQALE质量模型分解的示例。表中第一列把代码质量细分为可维护性、可测性、可变更性和可靠性几个维度,对于每个维度又有进一步的细节,如可测性又细分为单元测试可测性和集成级可测性这样的子特征,进一步的,子特征还能细化到可直接度量的属性,或者称为要求(表中第三列,即我们通常说的代码扫描规则),例如单元测试可测性再细分为“模块测试路径数量<11”和“模块调用参数数量<6”这样的规则:
表1 SQALE质量模型示例(Java语言,节选)
注:我们使用的SonarQube并没有完全照般SQALE的质量模型,在5.4及之前的版本中还存在与SQALE类似的可测性、易变更性、可理解性和可读性等维度,整个模型只有两级,即第一列和第二列合并了,例如可测性维度下直接对应了“表达式不应该太复杂”,“方法不应该太复杂”,“方法不应该有太多参数”等规则。在5.4之后的版本,即目前使用的版本则进一步简化,代码质量对应的扫描规则直接归属于“坏味道”大类,具体的规则可以打上多种标签来归类,分类和配置更加灵活。
代码质量的度量
那么,这些规则应该怎么量化呢?或者说,如何度量代码违背规则的程度,而且这种度量是可以加总的,毕竟规则间差异很大,上文也解释过,直接按数量汇总肯定是不合理的。
怎么办呢?SQALE方法的分析模型解决了这个问题,由此我们也引出了本文中的第二个重要概念:技术债TechnicalDebts。
“技术债”这一概念最早出现在1992年,其本义是指,开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。这个定义暗示了这种“负债”是一种刻意的、理性的经过权衡的行为,后文中我们进一步探讨技术债务的类型时会指出这一定义仅仅代表了技术债中相对良性的一类,是一个比较“温和”的定义。此处我们关注的重点是使用技术债这一隐喻来帮助大家理解度量代码质量的方法。
既然谈的是“债”,自然就应该和钱有关了。因此,技术债的“本金”就定义为修复代码质量问题所需消耗人力资源估值,例如,针对java语言,修复一个圈复杂度为15的方法需要一个开发人员15分钟的时间(以sonar java分析器缺省设置为例),这个值就是负债的本金。代码扫描工具中对应代码质量的每条扫描规则都对应着一个债务计算方法,有的规则是设定了固定的债务值,有的则根据违规程度有相应的计算公式。引入技术债的概念后,SQALE方法就可以把不同规则对应的代码质量度量统一为人力资源的消耗这一单一指标上。
根据图2质量模型所示由右向左的方向逐级汇总,就可以得到待评价软件的代码质量度量值。我们的其中一个度量难题:如何客观评价代码的质量,由此就得到了解答。
技术债的利息
关于技术债另外还有一个概念值得在这儿强调一下,即负债的利息。我们知道,通常借钱是有利息的,有的负债利息很低(如安居计划利息为0),有的利息较高(如信用卡欠款),有的则高到令人绝望(如高利贷)。同样,技术债也是有利息的,存在利滚利的情况,有的违规项马上修复要10分钟,如果放着不管一段时间后,也许就需要20分钟甚至更多的时间来修复(由于代码细节的知识随时间流逝,以及破窗效应造成代码问题加速恶化等原因)。有的代码扫描工具会针对规则定义本金和利息的计算方法,如Coder Gears的CppDepend,我们目前使用的SonarQube平台上的代码扫描插件不支持计算利息,因此本文就不过多讨论,大家只需要记住,因为利息的存在,技术债务不及时偿还的话,会在未来呈现出非线性增长,造成始料不及的损失。后续文章在讨论技术债的危害时,我们还会时常提及技术债的非线性特征。
不同类型代码的比较
现在我们还剩下一个度量问题:如何知道两段代码的质量差异?现在有了技术债本金这个绝对值,但是不同规模,不同类型的代码应该如何比较呢?SQALE方法中继续借鉴了“负债率”这个术语,计算公式为:偿还债务所需耗费的资源(即本金)除以重写所有代码的预估耗费的资源。在扫描工具的实现中,分母是通过代码量和开发生产力水平计算得出,其中的生产力是一个配置项,如SonarQube上可以配置编写一行代码的平均估计耗时。SQALE进一步使用了术语“债务等级”,定义了从A(非常好)到E(非常差)五个等级,根据负债率数值所在区间对应不同的等级,例如SonarQube中缺省[0, 5%]是A,(5%, 10%]是B,(10%,20%]是C,(20%, 50%]是D,高于50%是E。当负债率达到100%时,即债务开始超过资产,资不抵债,这时就称这种情况为“技术破产”。当然,日常工作中碰到这种情况时,我们不会用这么吓人的术语,通常是打着“重构”的旗号重写一遍。
下图是CppDepend的一个扫描汇总结果的示例,包含了我们讨论的所有概念(使用CppDepend为例是为了展示更全面的信息)。
图3技术债度量示例(CppDepend)
上图中工具扫描的代码行数为19862行,共负债32天,债务的年息是9天2小时,负债率是6.39%,债务等级是B级。
我们日常工作使用的工具平台是SonarQube,如下图所示:
图4技术债度量示例(SonarQube)
图中的项目负债12天,共有923个坏味道(即违规项数量),负债率(图中翻译为“技术债务比率”)为6.3%,债务等级(图中为SQALE评级)为B级。
SQALE给我们提供一套有效合理衡量代码质量的方法和工具,下图中SQALE方法流程清晰的展示了整个方法流程各个环节:
图5 SQALE方法流程
有了方法和工具(SonarQube)的支持,我们可以看看我们自己的代码质量是个什么状况。从扫描结果来看,与一些优秀的开源项目相比,我们还是有一些差距。部门EP(Engineering Productivity)极社根据扫描结果,挑选出了比较重要的以下4条规则:
Source files should nothave any duplicated blocks,
Classes should not becoupled to too many other classes,
Methods should not be toocomplex,
Control flow statements"if", "for", "while", "switch" and"try" should not be nested too deeply.
注:SonarQube中有些语言对应的扫描插件不支持第2条规则,如C++和Python。
这4条规是我们需要优先偿还的技术债,目前已经在整个部门推广实施。
读到这里,很多人也许忍不住想问,如此这般折腾有啥用?代码质量相对不高也没有影响到公司业务呀,提高这种代码质量除了让我们忙上加忙外,能有什么好处?或者说有什么价值?跟我的KPI有啥关系?
好吧,既然代码质量不好就是“负债”,那么欠债还钱不就是天经地义么,毕竟“出来混,迟早要还的。”显然这样的苍白说教无法服众,所以我们后续文章的重点就是深入理解技术债,深入分析提升代码质量的必要性和紧迫性。
So:读者朋友们,你们所在的团队或组织是否也在重视代码质量呢?
给大家推荐一个程序员学习交流一群:878249276,群里有分享的视频,面试指导,架构资料,还有思维导图
群公告有视频,都是干货的,你可以下载来看。主要分享分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频。