监控清晰度

    技术2024-03-26  87

    每个开发人员对于代码质量的含义都有自己的见解,并且大多数人都对如何发现编写不佳的代码有想法。 甚至代码气味一词也已进入集体词汇表,以描述需要改进的代码。

    气环什么?

    对于与本文有关的问题或与代码质量有关的任何问题,请访问由Andrew Glover主持的“ 改进Java代码质量”讨论论坛。

    有趣的是,通常与开发人员直接分道扬code的一种代码味道是太多的代码注释的味道。 有些人声称明智的代码注释是一件好事,而另一些人则认为它仅用作解释过于复杂的代码的机制。 显然,Javadocs™有一个有用的目的,但是多少个内联注释足以维护代码? 如果代码编写得足够好,它是否应该自我解释?

    这告诉我们有关代码气味作为评估代码的一种机制,这是主观的。 我认为非常糟糕的代码可能是别人写过的最好的作品。 以下短语听起来很熟悉吗?

    当然,(乍一看)这有点令人困惑,但请看它的可扩展性!

    要么

    这使您感到困惑,因为您显然不了解模式。

    我们需要的是一种客观地评估代码质量的方法,可以肯定地告诉我们我们正在查看的代码有风险。 信不信由你,那东西存在! 客观评估代码质量的机制已经存在了很长一段时间,只是大多数开发人员都忽略了它们。 它们被称为代码指标。

    代码指标的历史

    几十年前,一些超级聪明的人开始研究代码,希望定义可以与缺陷相关的测量系统。 这是一个有趣的主张:通过研究错误代码中的模式,他们希望创建正式的模型,然后对其进行评估以在缺陷变成缺陷之前对其进行评估。

    在此过程中,其他一些超级聪明的人也决定通过研究代码来查看他们是否可以衡量开发人员的生产力 。 从表面上看,每个开发人员的经典代码行度量似乎足够公平:

    乔比比尔产生的代码更多; 因此,乔的工作效率更高,值得我们付给他每一分钱。 另外,我注意到比尔在水冷却器上闲逛了很多。 我认为我们应该解雇比尔。

    但是实际上,这种生产率指标令人失望,主要是因为它很容易被滥用。 一些代码度量包括内联注释,而该度量实际上支持剪切和粘贴样式的开发。

    乔写了很多缺陷! 其他所有缺陷都分配给他。 我们解雇了Bill真是太糟糕了-他的代码几乎没有缺陷。

    可以预见的是,生产力研究被证明是非常不准确的,但是在度量标准被急切地考虑每个人的能力价值的管理机构广泛使用之前,并没有出现这种情况。 开发人员社区的激烈React是合理的,对于某些人,这种痛苦的感觉从未真正消失过。

    钻石原石

    尽管有这些失败,但在那些复杂性与缺陷相关性研究中还是有一些瑰宝的。 大多数开发人员早就忘记了它们,但是对于那些精打细算的人-尤其是在追求代码质量的过程中-今天应用它们会发现价值。 例如,您是否曾经注意到有时很难遵循长方法? 是否曾经在过深的嵌套条件中理解逻辑有困难? 您避开此类代码的本能是正确的。 长的方法和具有大量路径的方法很难理解,有趣的是,它们往往与缺陷相关。

    我将通过一些示例向您展示我的意思。

    数字海洋

    研究表明,普通人有能力处理其头部中的大约七个数据,正负两个。 这就是为什么大多数人可以轻松记住电话号码,但是却很难记住信用卡号,发射序列和其他高于7的数字序列的原因。

    该原理也适用于理解代码。 您可能之前已经看到了清单1中的代码片段:

    清单1.工作中的数字
    if (entityImplVO != null) { List actions = entityImplVO.getEntities(); if (actions == null) { actions = new ArrayList(); } Iterator enItr = actions.iterator(); while (enItr.hasNext()) { entityResultValueObject arVO = (entityResultValueObject) actionItr .next(); Float entityResult = arVO.getActionResultID(); if (assocPersonEventList.contains(actionResult)) { assocPersonFlag = true; } if (arVL.getByName( AppConstants.ENTITY_RESULT_DENIAL_OF_SERVICE) .getID().equals(entityResult)) { if (actionBasisId.equals(actionImplVO.getActionBasisID())) { assocFlag = true; } } if (arVL.getByName( AppConstants.ENTITY_RESULT_INVOL_SERVICE) .getID().equals(entityResult)) { if (!reasonId.equals(arVO.getStatusReasonID())) { assocFlag = true; } } } }else{ entityImplVO = oldEntityImplVO; }

    清单1显示了多达九种不同的路径。 该代码段实际上是350多行方法的一部分,该方法具有41条不同的路径。 想象一下,如果您被授权修改此方法以添加新功能。 如果您未编写方法,您是否认为可以进行必要的更改而不会引入缺陷?

    当然,您会编写一个测试用例,但是您是否认为您的测试用例可以隔离特定条件变更中的特定更改?

    测量路径复杂度

    在我之前提到的那些研究中开创的圈复杂度可以精确地测量路径复杂度。 通过计算通过方法的不同路径,此基于整数的度量标准恰当地描述了方法的复杂性。 实际上,多年来的各种研究已经确定,具有大于10的圈复杂度(CC)的方法具有较高的缺陷风险。 因为CC代表通过方法的路径,所以对于确定要达到100%的方法覆盖率需要多少测试用例,这是一个极好的数字。 例如,以下代码(您可能在本系列的第一篇文章中可能会记得)包含一个逻辑缺陷:

    清单2. PathCoverage有一个缺陷!
    public class PathCoverage { public String pathExample(boolean condition){ String value = null; if(condition){ value = " " + condition + " "; } return value.trim(); } }

    作为回应,我可以编写一个测试,以达到100%的行覆盖率:

    清单3.一个测试产生了全部覆盖!
    import junit.framework.TestCase; public class PathCoverageTest extends TestCase { public final void testPathExample() { PathCoverage clzzUnderTst = new PathCoverage(); String value = clzzUnderTst.pathExample(true); assertEquals("should be true", "true", value); } }

    接下来,我运行一个代码覆盖率工具,例如Cobertura,并得到如图1所示的报告:

    图1. Cobertura报告

    好吧,真令人失望。 代码覆盖率报告指出100%的覆盖率; 但是,我们知道这是一种误导。

    二对二

    请注意,清单2中的pathExample()方法的CC为2(一个用于默认路径,一个用于if路径)。 使用CC作为更精确的覆盖率衡量标准意味着需要第二个测试用例。 在这种情况下,这将是不进入if条件所采用的路径,如清单4中的testPathExampleFalse()方法所示:

    清单4.少走几步
    import junit.framework.TestCase; public class PathCoverageTest extends TestCase { public final void testPathExample() { PathCoverage clzzUnderTst = new PathCoverage(); String value = clzzUnderTst.pathExample(true); assertEquals("should be true", "true", value); } public final void testPathExampleFalse() { PathCoverage clzzUnderTst = new PathCoverage(); String value = clzzUnderTst.pathExample(false); assertEquals("should be false", "false", value); } }

    如您所见,运行此新测试用例会产生讨厌的NullPointerException 。 这里有趣的是,我们能够使用圈复杂度而不是代码覆盖来发现此缺陷。 代码覆盖率表明我们在完成一个测试用例之后就完成了,但是CC强迫我们再编写一个。 还不错吧?

    幸运的是,在这种情况下,所测试的方法的CC仅为2。想象一下,如果该缺陷被CC值为102的方法掩盖了,那么祝您好运!

    图表上的CC

    Java开发人员可以使用一些开源工具来报告圈复杂性。 JavaNCSS就是这样一种工具,它可以通过检查Java源文件来确定方法和类的长度。 而且,此工具还可以在代码库中收集每种方法的圈复杂度。 通过通过其Ant任务或通过Maven插件配置JavaNCSS,您可以生成列出以下内容的XML报告:

    每个包中类,方法,无注释的代码行以及各种注释样式的总数。 每个类中代码,方法,内部类和Javadoc注释的非注释行的总数。 代码库中每种方法的非注释行总数和循环复杂度。

    该工具附带了一些样式表,可用于生成汇总数据HTML报告。 例如,图2演示了Maven生成HTML报告:

    图2. Maven生成的JavaNCSS报告

    该报告的标为包含NCSS最多的前30个函数的部分详细介绍了代码库中最大的方法,这些方法几乎总是与包含最高循环复杂度的方法相关。 例如,该报告列出了类DBInsertQueue的updatePCensus()方法,其非updatePCensus()行数为283,循环复杂度(标记为CCN)为114。

    如上所示,循环复杂度是代码复杂度的良好指标; 此外,它是开发人员测试的绝佳晴雨表。 一个好的经验法则是创建一些测试用例,使其等于要测试的代码的圈复杂度值。 对于图2所示的updatePCensus()方法,您需要114个测试用例才能实现完全覆盖。

    分而治之

    面对表示高圈复杂度值的报告时,首要行动是验证任何相应测试的存在。 如果有测试,有几项? 除了最罕见的代码库之外,所有其他代码库实际上都具有114个用于updatePCensus()方法的updatePCensus()实际上,为一个方法编写许多测试用例可能会花费很长时间)。 但是,即使很少,对于减少该方法存在缺陷的风险也是一个不错的开始。

    如果没有任何相关的测试用例,则显然您需要测试该方法。 您首先想到的是该进行重构的时候了,但是这样做会破坏重构的第一个规则,即编写一个测试用例。 首先编写测试用例可以降低重构的风险。 降低循环复杂度的最有效方法是提取部分代码并将其放入新方法中。 这将复杂性推到了更小,更易于管理(因此更具可测试性)的方法中。 当然,然后应该测试那些较小的方法。

    在连续集成环境中,可以随着时间的推移评估方法的复杂性。 首次运行报告后,您可以监视方法的复杂度值或任何相关的增长。 如果发现CC有所增加,则可以采取适当的措施。

    如果方法的CC值持续增长,则有两种响应选项:

    确保存在健康的相关测试量,以降低风险。 评估重构方法以减少任何长期维护问题的可能性。

    还要注意,JavaNCSS并不是Java平台上唯一可以简化复杂性报告的工具。 PMD是另一个分析Java源文件的开源项目,它具有一系列规则,其中一个报告了圈复杂度。 CheckStyle是另一个具有类似圈复杂度规则的开源项目。 PMD和CheckStyle都具有Ant任务和Maven插件。 (请参阅相关主题以获取更多关于所有的工具,到目前为止讨论。)

    使用复杂性指标

    由于循环复杂度是代码复杂度的良好指标,因此测试驱动的开发与较低的CC值之间存在很强的关系。 当测试经常写(注意,我没有暗示第一 ),开发人员必须编写简单的代码,因为复杂的代码难以测试的倾向。 如果您发现编写测试有困难,则有一个危险信号:被测代码可能很复杂。 在这种情况下,TDD的“代码,测试,代码,测试”周期很短,因此需要进行重构,从而不断推动复杂代码的开发。

    因此,在使用遗留代码库的情况下,测量圈复杂度特别有价值。 此外,与分散的开发团队甚至在具有各种技能水平的大型团队中监视CC值可能会有所帮助。 在代码库中确定类方法的CC并不断监视这些值,将使您的团队在解决复杂性问题时能抢先一步 。


    翻译自: https://www.ibm.com/developerworks/java/library/j-cq03316/index.html

    相关资源:jdk-8u281-windows-x64.exe
    Processed: 0.009, SQL: 9