重构评估与度量

在接触代码之前,我们可以通过一些现成的工具,来对现有的项目进行一些不评估,并通过度量来提供指标。

识别技术债务

对于技术债务,它的利息表现为系统的不稳定性,以及由于临时性手段和缺乏合适的设计、文档工作和测试带来的不断攀升的维护成本。 —— 《软件架构师应该知道的 97 件事》

如 Robert Nord 提出的 “技术债务全景图”(Tech Debt Landscape) 所示:

技术债全景图

技术债对于软件的影响:可维护性(Maintainability)、可演进性(Evolvability),而这些技术债对于非技术人员来说都是不可见的。它们源于生活,藏于黑暗中。

技术债风暴

重构开始之前,我们可以进行技术债的头脑风暴,收集每个开发人员每迫切解决的技术痛点。并按照优先级来评估这些技术债,列入我们的重构范围中。

如我的同事在《技术债治理的四条原则》 一文中所介绍的,我们可以在对应的限界上下文里,可视化技术债:

Classified Technical Debt Mapping

再根据 “核心领域优于其他子域” 的原则,及其严重程度,来划分出技术债的优先级。

架构评估:技术驱动 vs 业务驱动

如我在那篇 《分层架构重构》 中所说,在大量的现有系统中,我们发现了 MVC 架构模式被落地为三层分层架构(controller-service-model)。开发人员对它们的错误等同,导致了架构上的一系列错误。

对于简单的系统来说,CSM 的包结构问题不大。或者说,对于非常简单的系统来说,大泥球架构也没有问题。我们所针对的是那些中大规模的系统。在这些系统里,系统并非一次性的,开发出来就不再维护了。因此,它们需要对更合适的架构设计和包的拆分分。

借助于 Tequila 这样的架构可视化工具,又或者是 coca arch,便可以得到项目的调用关系图,它可以在某种层面上反应出系统的架构。根据它,我们可以知道:

  • 项目的结构划分是否合理
  • 查看项目的代码中是否存在循环依赖的情况

结果如下图所示:

Coca Call Graph

通过调用关系图,我们也可以查看类之间、包之间是否存在相互依赖。

代码评估:收集 bad smell

对于这部分内容来说,你可以直接采用成熟的商业工具,如 SonarQube 便可以完成这方面的工作。

你也可以通过 coca bs 来做一些简单的 Bad Smell 收集:

{
   "dataClass": [
      {
         "File": "examples/api/BookController.java",
         "BS": "dataClass"
      }
   ],
   "lazyElement": [
      {
         "File": "examples/api/model/BookRepresentaion.java",
         "BS": "lazyElement"
      }
   ]
}

而后,再生成对应的重构建议。

收集 Todo

代码中的 Todo 注释,是一些本应该发生的事情,本应该做好,但是我没有立即去做。换句话来说,Todo 都是项目中的技术债务,就了可能就永远不会做。

所以,我们需要有工具来查找项目的 Todo,如笔者编写的 Coca,可以寻找代码中的 Todo,包含其对应的日期、作者、提交信息、文件名及对其的行数等信息:

MESSAGES FILENAME LINE
happens on macosx, don’t know why …/ContributedLibraryTableCellJPanel.java 118
Make this a method of Theme …/ContributedLibraryTableCellJPanel.java 233
Do a better job in refreshing only the needed element …/LibraryManagerUI.java 241
Do a better job in refreshing only the needed element …/LibraryManagerUI.java 273
Make this a method of Theme …/MultiLibraryInstallDialog.java 149
happens on macosx, don’t know why …/ContributedPlatformTableCellJPanel.java 183
show error error when importing. ignoring :( …/Base.java 2423
Improve / move error handling …/Editor.java 1541
Should be a Theme value? …/EditorHeader.java 78
Should be a Theme value? …/EditorStatus.java 73
Improve decoupling …/EditorTab.java 465

随后,我们只需要根据真实的情况,更新项目中的 Todo,以确认出我们需要完成的技术债务。

不过,写好一个 Todo 并不是容易,万一以后大家都不写了呢?

测试和文档评估

  • 关于测试的话题,我们会有一个大的专题来介绍相关的活动。
  • 至于文档的缺乏,会在文中的最后介绍。
  • 不过,你也可以参考我的那篇《构建质量可信的软件系统》 来对你的文档进行评估。

项目评估

根据不同的项目,侧重点有所不同。但是毫无疑问地,我们可以统计:

  • 功能的 bug 率,对应的 bug 修改时间
  • bug 常见的问题
  • ……

你都懂的。我暂时就不 copy 了。

编写工具评估

在我遇到的一个重构项目中,项目中经常抛出Null Pointer Exception的问题。于是,我便写了一个简单的工具,来查找项目中返回Null Pointer Exception的代码,并对调用的地方进行评估。

随着评估的进一步深入,我在工具中加入了更多的功能,如:

  • 静态方法多,难以进行测试。要么是工具类过多,需要抽取基础设施;要么就是缺乏 OO 设计,导致过程性代码……。
  • Util 过多,同上。
  • Null Pointer Exception越多,则项目的出错可能性越多。
  • 类方法数的标准差,能判断出对应的上帝类情况。
  • 方法长度的标准差,越大则意味着方法的长度都比较长,方便于重构。

只需要运行 coca evaluate,就能得到以下的结果:

TYPE COUNT LEVEL TOTAL RATE
Nullable / Return Null 0 Method 1615 0.00%
Utils 7 Class 252 2.78%
Static Method 0 Method 1615 0.43%
Average Method Num. 1615 Method/Class 252 6.408730
Method Num. Std Dev / 标准差 1615 Class - 7.344917
Average Method Length 13654 Without Getter/Setter 1100 12.412727
Method Length Std Dev / 标准差 1615 Method - 20.047092

笑,你只要加强使用 TDD,那么上述的大部分问题,都能得到进一步的缓解。

代码评估工具

Java 世界流行的几个找问题工具:

  • FindBugs/SpotBugs
  • PMD/CPD
  • Checkstyle

试试你就知道了。

真实的测试覆盖率

尽管有越来越多的项目将测试覆盖率作为一项考核指标。但是,对于诸多编程实践本身就好的公司为说,测试覆盖率也往往不是真的。

我们编写测试的其中一个目的是用于快速反馈,即当我们的功能出现问题的时候,我们可以快速通过测试来定位到问题所。然而,如果那些是没有断言的测试,那么我们就无法通过它来进行快速反馈。即,如果我们重构过程中,修改了某一块的功能,可能会进一步导致出现 bug。

为此,你可以借助于 Coca 的 Test Bad Smell 功能,来找到对应的问题。只需要执行 coca tbs,便能帮助你找到代码中的坏味道。它可以在你进入重构之前,帮你看看是否有对应的风险。

如下是 Coca 扫描出来的 Arduino 开源项目测试问题:

TYPE FILENAME LINE
DuplicateAssertTest app/test/cc/arduino/i18n/ExternalProcessOutputParserTest.java 107
DuplicateAssertTest app/test/cc/arduino/i18n/ExternalProcessOutputParserTest.java 41
DuplicateAssertTest app/test/cc/arduino/i18n/ExternalProcessOutputParserTest.java 63
RedundantPrintTest app/test/cc/arduino/i18n/I18NTest.java 71
RedundantPrintTest app/test/cc/arduino/i18n/I18NTest.java 72
RedundantPrintTest app/test/cc/arduino/i18n/I18NTest.java 77
DuplicateAssertTest app/test/cc/arduino/net/PACSupportMethodsTest.java 19
DuplicateAssertTest app/test/processing/app/macosx/SystemProfilerParserTest.java 51
DuplicateAssertTest app/test/processing/app/syntax/PdeKeywordsTest.java 41
DuplicateAssertTest app/test/processing/app/tools/ZipDeflaterTest.java 57
DuplicateAssertTest app/test/processing/app/tools/ZipDeflaterTest.java 83
DuplicateAssertTest app/test/processing/app/tools/ZipDeflaterTest.java 109

好在上述的测试代码中,没有出现诸如于下面场景的测试坏味道:

  • EmptyTest。测试函数里空空如也
  • UnknownTest。测试中没有对应的断言
  • IgnoreTest。测试是被 Ingore 的,即不会运行的测试。

如果你的代码中出现了大量的上述问题,你需要好好反思一下,你的测试覆盖率是真实的吗?

可测试性评估

代码本身是缺乏测试的,那么它就是一个遗留系统。

度量

根据《精益软件度量》对于度量的定义:

  • 度量在组织上下文中形成的一系列共识
  • 将经验性模型转换为向量化模型(修改)
  • 包含人、流程、组织和工具的一个动态系统

度量缺陷

寻找专业人士

你懂的。

下一节:我喜欢重构的那种感觉 —— 把一坨烂代码,驯服成更易于阅读的代码。