CppDepend 技术债务估算与管理
简介
如今,技术债务这一比喻已被软件行业广泛采用。它由以下人士提出 Ward Cunningham 于1992年。
这 参考文章 Martin Fowler 的著作详细描述了技术债务的比喻。引用 M. Fowler 的话:
在这个比喻中,以快速而粗糙的方式做事会让我们背上技术债务,这类似于金融债务。与金融债务一样,技术债务会产生 利息 还款以未来开发中因快速而粗糙的设计选择而必须付出的额外努力的形式出现。我们可以选择继续支付利息,也可以通过将快速粗糙的设计重构为更好的设计来偿还本金。尽管偿还本金需要成本,但我们可以通过减少未来的利息支付而获益。
使用CppDepend, 代码规则可以通过 C# LINQ 查询编写。 应用于代码库时,规则会产生问题。提供了一个专门的债务 API,通过用 C# 编写的公式来估算问题的技术债务和年利息。问题的技术债务和年利息都以人工时间来衡量。
- 该 技术债务 是修复该问题所需的预计人工时间。
- 该 年利息 是如果问题不修复每年消耗的预计人工时间。这提供了对以下内容的估算 业务影响 问题的。
例如:
warnif count > 0
from m in Methods
where m.CyclomaticComplexity > 10
select new {
m,
m.CyclomaticComplexity,
Debt = (3*(m.CyclomaticComplexity-10)).ToMinutes().ToDebt(),
AnnualInterest = (m.PercentageCoverage == 100 ? 10 : 120).ToMinutes().ToAnnualInterest()
}在此示例中,该规则匹配过于复杂的方法,复杂度通过以下方式衡量 圈复杂度 代码度量。我们可以看到:
- 该 技术债务 与超过特定阈值的复杂度成正比。
- 该 年利息 如果方法被测试 100% 覆盖,则为每年 10 分钟,否则为每年 2 小时。
让一个复杂的方法既未重构又未被测试覆盖是一种容易出错的情况。往好里说,这种情况会损害代码的可维护性;往坏里说,最终会在生产环境中产生 bug。年利息估算的是 平均 如果复杂方法未被重构,每年的成本。如果该方法也没有被测试覆盖,情况会更糟。单词 平均 在这里突出显示是因为,例如,在 8 个复杂且未测试的方法中,可能只有一个存在 bug,而发现、调查、修复和交付该 bug 需要 2 人天(2×8 小时)的工作量。
默认规则集中的每条规则都包含用于计算每个问题的技术债务和年利息的公式。规则和公式可以创建和自定义,以更好地匹配您团队的需求和习惯,因为它们只是可以在 Visual Studio 中编辑的原始 C#。主要优点是 技术债务估算完全透明,并且可以使用 CppDepend 轻松定制。
年利息和严重性
年利息是的度量 问题严重性. 严重程度和年度利息代表相同的概念 其中年度利息是连续度量,而严重程度是离散度量。
CppDepend 定义了 5 个严重级别,问题的严重程度通过基于年度利息的阈值进行估算。
- 低:严重程度为低(Low)的问题代表一个小改进,一种让代码看起来更优雅的方式。默认年利息阈值:零或每年少于 2 人分钟。
- 中:严重程度为中(Medium)的问题代表一个警告,该问题即使不修复也不会对开发产生重大影响。默认年利息阈值:每年少于 20 人分钟。
- 高:严重程度为高(High)的问题应尽快修复,但可以等到下一个计划的间隔。默认年利息阈值:每年少于 2 人时。
- 严重:严重程度为严重(Critical)的问题不应进入生产环境。出于业务紧迫需要它仍然可以进入,但最迟必须在接下来的迭代中修复。默认年利息阈值:每年少于 10 人时。
- 阻断:Blocker 严重级别的问题不能进入生产环境,它 必须 被修复。默认年度利息阈值:每年超过 10 人时。
请注意的概念 严重问题 不同于的概念 严重规则。问题的严重程度与其规则是否为关键规则无关。可以将规则标记为关键以对其实施某些约束,例如可以编写一个在关键规则违规时失败的质量门禁。

债务设置
技术债务的计算和结果可以通过以下面板中的设置进行微调 CppDepend > 项目属性 > Issues and Debt.

你可以看到:
- 与问题严重程度和年度利息相关的阈值,已在上一节中说明
- 与 SQALE 债务评级相关的阈值将在下一节说明
- 两个可乘系数,可应用于所有技术债务和年度利息的估计值。默认情况下,这些系数设置为 1。
- 为了确保债务估算以有意义的人工时间度量显示,关于以下内容的设置 每天工作小时数 或 每年工作天数 可以调整。
- 还有一些设置可选择债务值的格式,以及转换 人时债务值 到 货币成本债务值.

SQALE债务比率和债务评级
该 SQALE方法 (通常读作 "scale")是评估技术债务的标准化方法。CppDepend 实现了 债务比率 和 债务评级 属于SQALE方法的一部分。
该 债务比率 在代码库或代码元素上,以估计技术债务相对于从头重写该代码元素所需估计工作量的百分比表示。从头重写代码元素所需的估计工作量是根据代码元素的代码行数以及……推断的 债务设置 命名为 开发 1,000 个逻辑代码行的预计人日数 (请参阅上一节关于债务设置的截图)。
值 开发 1,000 个逻辑代码行的预计人日数 设置只是一个估计值,因此在短期内没有意义。经过几个人月甚至人年的开发后,这个值通常足够稳定,可以用于估算。此估计设置还需要考虑编写单元测试的成本。默认值为 18 人天,代表平均 55 行新的逻辑代码行, 100%单元测试覆盖,每天每位开发者。
该 债务评级 代码库或代码元素的该值是根据应用于债务比率(Debt Ratio)的阈值推断的。债务评级(Debt Rating)范围为 A、B、C、D、E。四个阈值可在债务设置面板中自定义(请参阅上一节关于债务设置的屏幕截图)。默认阈值为:
- Debt Ratio 在 [0, 5%[ 区间时,债务评级为 A。
- Debt Ratio 在 [5%, 10%[ 区间时,债务评级为 B。
- Debt Ratio 在 [10%, 20%[ 区间时,债务评级为 C。
- Debt Ratio 在 [20%, 50%[ 区间时,债务评级为 D。
- Debt Ratio 达到 50% 或以上时,债务评级为 E。
代码库的 Debt Rating 和 Debt Ratio 值显示在仪表板中。在以下章节 浏览技术债务 我们将展示简单的 C# 代码查询如何显示任何代码元素的 Debt Ratio 和 Rating 值。

问题修复优先级排序与 Breaking-Point 指标
该 断点 某个问题或一组问题的该值,是从现在到修复该问题的估计成本达到不修复该问题的估计成本的时间点。
断点是 债务除以年利息。例如,如果修复债务的估计成本等于 10 人天,估计年利息等于每年 2 人天,那么盈亏平衡点等于从现在起 5 年。
请注意,一个断点如果是 低于 每年意味着在接下来的 12 个月内,预计修复债务比不修复更便宜。
另请注意,盈亏平衡点不像债务或年利息那样以人工时间(人月或人年)来衡量,而是以常规时长(月或年)来衡量。盈亏平衡点值使用 TimeSpan 类型。
当涉及到 优先处理要首先修复的问题,问题的严重程度是一个重要参数。提醒一下:严重程度是年利息的离散度量。因此,年利息越高,修复就越重要。
然而,在给定的严重程度级别下,并非所有问题都是同等的。有些问题需要更多的修复工作量。这通过技术债务度量来估算。因此,为了估算 投资回报率 (ROI) 对于问题修复,估算债务除以年利息是有意义的。这个估算值是盈亏平衡点,值越低,投资回报率(ROI)越高。
让我们明确一点:在默认规则集中,与自基线以来的新问题相关的问题,例如 API破坏性变更, 代码元素质量变得更差, 未测试的新代码元素……是产生更高年度利息、因而比其他问题严重程度更高的问题。这符合 优先修复最近引入问题的最佳实践.

浏览技术债务
在简介中我们看到 代码规则通过 C# LINQ 查询实现 我们还看到,债务和年度利息的估算是从嵌入在这些 LINQ 查询中的公式推导出来的。
这 C# LINQ查询方案 更进一步,可用于浏览和探索技术债务。域 问题 是在代码库中发现的所有问题的枚举。显然,依赖此域的查询在所有规则执行完毕后才会执行。
例如,当点击仪表板上的问题数量时,如 自基线以来的新重大问题 在下面的示例中,会生成一个代码查询来列出相关问题。请注意,问题可以按规则或按代码元素分组。在下面的屏幕截图中,问题按规则分组。

注意 Dashboard 上的 Explore Debt 菜单,它会针对规则、问题和代码元素生成一些查询,以深入探索技术债务。

右键单击 规则类别,像 代码覆盖率 此处的类别显示用于查询此类别问题的菜单:

一些默认的债务和问题查询可在以下位置找到 热点 组。例如查询 类型热点 首先列出债务最多的类型。

该 规则 域是所有活动规则的枚举。它列出了已违反和未违反的规则。可以编写查询按债务和问题数量列出规则。匹配的规则可以按类别分组。
毫不奇怪,覆盖率、代码质量和架构往往是产生最多债务和问题的类别。

基线在探索问题集时起主要作用,因为自基线以来的新问题或已修复问题可以评估近期工作的质量。
默认情况下,基线是最接近 30 天前的历史分析结果,并且默认情况下,历史分析结果最多每天保存一次。
因为在评估近期工作质量时,人们肯定希望在昨天、上周和上月的基线之间切换,CppDepend 仪表板允许您应用一个 临时 一键设置基线。然后债务和问题集会在几秒钟内相应地重新计算。
由于自基线以来评估问题和债务很重要(正如我们刚才所见),所有 热点 默认查询附带 自基线以来 版本。例如,下面是一个用于评估的查询 按规则的新债务和问题 自基线以来。

让我们提一下关于债务和问题查询的一个细微之处。类型包含方法和字段,命名空间包含类型,程序集包含命名空间。因此,类型、命名空间和程序集都是代码元素的父级。
所有问题相关 ICodeElement 扩展方法如 elem.Debt(), elem.AnnualInterest(), elem.Issues(),版本前缀为 全部 返回父代码元素及其所有子元素的债务和问题。因此:
- elem.AllIssues() 返回父代码元素中的问题及其子代码元素上的问题的枚举。在产品中,我们有时会使用以下术语 累积问题 父代码元素(如程序集、命名空间或类型)的。
- elem.AllDebt() 返回父代码元素及其子代码元素的估算债务总和。
- elem.AllAnnualInterest() 返回父代码元素及其子代码元素的估算年度利息总和。
- elem.AllBreakingPoint() 返回父代码元素及其子代码元素的估算临界点。
技术债务和质量门
您将找到与技术债务和问题相关的默认质量门,包括 债务百分比, 自基线以来的新债务 或 新的阻断/严重/主要问题。与绝对技术债务值相关的质量门禁默认处于禁用状态,因为正确的阈值只能在特定项目的上下文中定义。

以同样的方式 问题 和 规则 被预定义为可查询的域,提供问题或规则的可枚举集合,域 QualityGates 是质量门禁的枚举。下面的默认查询估算自基线以来的质量门禁趋势。请注意,依赖基线的质量门禁(如 自基线以来的新债务) 在基线上既未定义值也未定义状态。

技术债务可能为零或不完整的原因
- 我的技术债务估算显示为零或?
如果技术债务为零或 ?,您很可能正在分析用旧版本 CppDepend(v6 或更低版本)创建的项目。早期 CppDepend 版本的规则集没有债务公式,因此默认情况下,没有债务公式的问题的债务为零。
在 仪表板 > 债务面板 您应该看到一个名为的链接 用默认规则创建规则文件.
单击此链接将自动创建一个规则文件,其中包含所有新的默认规则,即带有债务估算公式的规则。完成后,建议用估算技术债务的规则替换实际的项目规则。为此,可以从……使用拖放 查询和规则浏览器 面板(规则面板和规则组面板)。请注意,当债务公式被较低版本的 CppDepend(v6 及更低版本)读取时会导致规则编译错误。如果您打算在 CppDepend v6 或更低版本中使用此项目,请先克隆它。
对于自定义规则,我们建议修改其源代码以编写自定义债务估算公式。
最后,请注意,默认规则文件将在与项目文件相同的目录中创建,并以相对文件路径附加到项目中。此路径可以从……进行编辑 CppDepend 项目属性 > Paths Referenced. - 我的技术债务估算不完整,因为未提供代码覆盖率数据
未经测试或仅被单元测试部分测试的代码是技术债务的一大来源。实际上,每一行未被测试覆盖的代码都会增加技术债务。这就是为什么当……时,仪表板的 Debt 部分会显示警告消息 代码覆盖率文件导入 未在CppDepend项目中设置。
- 基线上没有代码覆盖率数据
当代码覆盖率在当前分析结果中可用但在基线分析结果中不可用时,与代码覆盖率相关的规则不会产生问题。确实,在这种情况下,无法在基线上估算覆盖率问题,所有覆盖率问题都会显示为新问题。
这种情况通常出现在项目已创建但获得的第一个分析结果不包含覆盖率数据时。在 CppDepend 项目中,默认基线设置是选择最接近……的基线分析结果 30天前获得,所以这个问题可能会持续一个月。
通常 以解决此情况,我们建议删除没有代码覆盖率数据的历史分析结果。为此,您需要打开包含在……中定义的历史分析结果的文件夹 CppDepend 项目属性 > Analysis > Baseline for Comparison > Historic Analysis Results (默认设置为项目输出文件夹)。然后找到包含要删除的历史分析结果的文件夹,直接删除该文件夹。
例如,在下方的屏幕截图中,所选文件夹表示 2016 年 12 月 13 日上午 8:59 获得的历史分析结果。
我们理解这种手动调整文件夹的方式并不是解决此类情况的最佳方法。如果您希望我们提供一个 UI,列出历史分析结果,显示哪些没有覆盖率数据(或其他缺陷,如源代码未解析),并允许删除它们,请告知我们。
我们还可以在分析时提供一个过滤器,如果分析结果不满足某些条件(如覆盖率数据可用),则不将其保存为历史记录。
