在接触代码之前,我们可以通过一些现成的工具,来对现有的项目进行一些不评估,并通过度量来提供指标。
识别技术债务
对于技术债务,它的利息表现为系统的不稳定性,以及由于临时性手段和缺乏合适的设计、文档工作和测试带来的不断攀升的维护成本。 —— 《软件架构师应该知道的 97 件事》
如 Robert Nord 提出的 “技术债务全景图”(Tech Debt Landscape) 所示:
技术债对于软件的影响:可维护性(Maintainability)、可演进性(Evolvability),而这些技术债对于非技术人员来说都是不可见的。它们源于生活,藏于黑暗中。
技术债风暴
在重构开始之前,我们可以进行技术债的头脑风暴,收集每个开发人员每迫切解决的技术痛点。并按照优先级来评估这些技术债,列入我们的重构范围中。
如我的同事在《技术债治理的四条原则》 一文中所介绍的,我们可以在对应的限界上下文里,可视化技术债:
再根据 “核心领域优于其他子域” 的原则,及其严重程度,来划分出技术债的优先级。
架构评估:技术驱动 vs 业务驱动
如我在那篇 《分层架构重构》 中所说,在大量的现有系统中,我们发现了 MVC 架构模式被落地为三层分层架构(controller-service-model)。开发人员对它们的错误等同,导致了架构上的一系列错误。
对于简单的系统来说,CSM 的包结构问题不大。或者说,对于非常简单的系统来说,大泥球架构也没有问题。我们所针对的是那些中大规模的系统。在这些系统里,系统并非一次性的,开发出来就不再维护了。因此,它们需要对更合适的架构设计和包的拆分分。
借助于 Tequila 这样的架构可视化工具,又或者是 coca arch
,便可以得到项目的调用关系图,它可以在某种层面上反应出系统的架构。根据它,我们可以知道:
- 项目的结构划分是否合理
- 查看项目的代码中是否存在循环依赖的情况
结果如下图所示:
通过调用关系图,我们也可以查看类之间、包之间是否存在相互依赖。
代码评估:收集 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 的,即不会运行的测试。
如果你的代码中出现了大量的上述问题,你需要好好反思一下,你的测试覆盖率是真实的吗?
可测试性评估
代码本身是缺乏测试的,那么它就是一个遗留系统。
度量
根据《精益软件度量》对于度量的定义:
- 度量在组织上下文中形成的一系列共识
- 将经验性模型转换为向量化模型(修改)
- 包含人、流程、组织和工具的一个动态系统
度量缺陷
寻找专业人士
你懂的。
下一节:我喜欢重构的那种感觉 —— 把一坨烂代码,驯服成更易于阅读的代码。