在过去的几年里,我曾看过很多项目的大量源代码,从精美的设计到像是用胶带绑定到一起的代码。我写过新的代码也维护过其他开发人员的源代码。我喜欢编写新的代码,但也喜欢采用一些现有的代码,以某种方法将其简化或将重复的代码提取到一个公共类中。在我早期的工作生涯中,许多人都认为如果不编写新的代码就不会有好的效率。幸好,在 20 世纪 90 年代末,Martin Fowler 编写了 Refactoring一书(参见 参考资料),它使得在不改变外部行为的前提下改进现有代码成为可能。
我在 本系列中所一直推崇的就是 效率:如何减少耗时过程的冗余度,更快速地执行它们。在本文的任务中,我一样推崇这个目标,并且将论述怎样更 有效地执行它们。
重构的一个典型方法是在引入新代码或更改方法时对现有代码做出小小的变动。该技巧面临的挑战在于一个开发团队的开发人员的应用方法不一致,并且很容易错失重构的机会。这也正是我提倡使用静态分析工具识别编码违规的原因所在。有了这些工具,您就能够从总体上了解代码库,并且处于类或方法的级别。幸运的是,在 Java™程序设计中,您可以选择的可免费下载的开源静态分析工具很多:CheckStyle、PMD、FindBugs、JavaNCSS、JDepend 等等。
在本文中,您将学习如何:
- 使用 CheckStyle 度量 圈复杂度(cyclomatic complexity),并提供诸如 Replace Conditional with Polymorphism之类的重构,以此来减少 条件复杂度代码味道
- 使用 CheckStyle 评估 代码重复率,并提供诸如 Pull Up Method之类的重构,以此来移除 重复代码
- 使用 PMD(或 JavaNCSS)计算 源代码行,并提供诸如 Extract Method之类的重构,以此来淡化 大类代码味道
- 使用 CheckStyle(或 JDepend)确定一个类的 传出耦合度(efferent coupling),并提供诸如 Move Method之类的重构,以此来除掉 过多的导入代码味道
我将使用如下的通用格式来检查每一种代码味道:
- 描述可以指示出代码里面的问题的味道
- 定义可以找到该味道的度量方法
- 展示可以度量代码味道的工具
- 提供用于修复代码味道的重构和模式(在某些情况下)
实质上,这个方法提供了一个找到和修复整个代码库中的代码味道的一个框架。这样您就可以更好地了解到代码库中较危险的部分,然后再做出更改。更好的是,我还会向您展示如何将这个方法集成到自动构建中。
条件复杂度
味道:条件复杂度
度量:圈复杂度
工具:CheckStyle、JavaNCSS 以及 PMD
重构:Replace Conditional with Polymorphism、Extract Method
味道
条件复杂度可以以几种不同的方式出现在源代码中。这种代码味道的一个例子就是含有多个条件语句,如 if
、while
或者 for
语句。另一种条件复杂度是以 switch
语句的形式呈现出来的,如清单 1 所示:
清单 1. 使用 switch
语句来执行条件行为
...
switch (beerType) {
case LAGER:
System.out.println("Ingredients are...");
...
break;
case BROWNALE:
System.out.println("Ingredients are...");
...
break;
case PORTER
System.out.println("Ingredients are...");
...
break;
case STOUT:
System.out.println("Ingredients are...");
...
break;
case PALELAGER:
System.out.println("Ingredients are...");
...
break;
...
default:
System.out.println("INVALID.");
...
break;
}
...
|
switch
语句本身并没有不妥。但当一个语句包含太多的选择和代码时,它就可能暗示有需要重构的代码。
度量
要确定条件复杂度代码味道,需要确定方法的 圈复杂度。圈复杂度是一种度量方法,由 Thomas McCabe 于 1975 年定义。圈复杂度数(Cyclomatic Complexity Number,CCN)度量一个方法中某一路径的数量。无论一个方法中有多少条路径,它的起始 CNN 都从 1 开始。每一个条件构造,如 if
、switch
、while
和 for
语句,都被分配一个 1 值和异常路径。一个方法的总的 CCN 表明了它的复杂度。很多人认为当 CCN 为 10 或超过 10 时,就表明该方法过于复杂。
工具
CheckStyle、JavaNCSS、以及 PMD 都是度量圈复杂度的开源工具。清单 2 展示了用 XML 定义的 CheckStyle 规则文件的一个代码片断。CyclomaticComplexity
模块定义了一个方法的 CCN 的最大限度。
清单 2. 配置 CheckStyle,查找圈复杂度为 10 或大于 10 的方法
<module name="CyclomaticComplexity">
<property name="max" value="10"/>
</module>
|
用清单 2 的 CheckStyle 规则文件、清单 3 的 Gant 例子来示范如何将 CheckStyle 作为一个自动构建的一部分来运行。(参见 什么是 Gant ?侧边栏):
清单 3. 使用 Gant 脚本来执行 CheckStyle 检查
target(findHighCcn:"Finds method with a high cyclomatic complexity number"){
Ant.mkdir(dir:"target/reports")
Ant.taskdef(name:"checkstyle",
classname:"com.puppycrawl.tools.checkstyle.CheckStyleTask",
classpathref:"build.classpath")
Ant.checkstyle(shortFilenames:"true", config:"config/checkstyle/cs_checks.xml",
failOnViolation:"false", failureProperty:"checks.failed", classpathref:"libdir") {
formatter(type:"xml", tofile:"target/reports/checkstyle_report.xml")
formatter(type:"html", tofile:"target/reports/checkstyle_report.html")
fileset(dir:"src"){
include(name:"**/*.java")
}
}
}
|
清单 3 中的 Gant 脚本创建了图 1 中展示的 CheckStyle 报告。该图下面的部分指示出了一个方法的 CheckStyle 圈复杂度违规。
图 1. CheckStyle 报告根据过高的 CCN 来指示一种方法失败

重构
图 2 为用 UML 表示的 Replace Conditional with Polymorphism重构:
图 2. 用多态替代条件语句

单击 此处查看完整图。
在图 2 中,我:
- 创建了一个叫做
BeerType
的 Java 界面
- 定义了一个通用的
showIngredients()
方法
- 为每一个
BeerType
创建了一个实现类
为了使文章保持简洁,我仅为每一个类提供一个方法的实现。显然,创建一个界面的方法可能不只一个。重构能够使代码更易于维护,如 Replace Conditional with Polymorphism 和 Extract Method(本文稍后将会讨论)。
回页首
重复代码
味道:重复代码
度量:代码重复率
工具:CheckStyle、PMD
重构:Extract Method、Pull Up Method、Form Template Method、Substitute Algorithm
味道
重复代码可能在代码库中悄然发生。有时,复制粘贴某些代码要比将该行为泛化到另一个类更简单。但复制粘贴的方法存在一个问题,即它强制将代码复制多份,并且需要维护。而且当复制出的代码发生轻微的变化而引发行为不一致时,就会发生更不易察觉的问题,具体取决于哪个方法在执行该行为。清单 4 是一个关闭代码库连接的代码示例,相同的代码出现在两种方法中:
清单 4. 重复代码
public Collection findAllStates(String sql) {
...
try {
if (resultSet != null) {
resultSet.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
catch (SQLException se) {
throw new RuntimeException(se);
}
}
...
}
...
public int create(String sql, Beer beer) {
...
try {
if (resultSet != null) {
resultSet.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
catch (SQLException se) {
throw new RuntimeException(se);
}
}
...
}
|
度量
查找重复代码的度量方法是在代码库中的类的内部和其他类之间搜索代码重复。没有工具的话,类间的重复就更难评估。由于复制的代码通常都会发生一些轻微的变化,因此不仅要度量完全相同的代码,而且要度量 相似的代码,两者都很重要。
工具
PMD 的 Copy/Paste Detector(CPD)与 CheckStyle 这两种开源工具可以用于在整个 Java 代码库中查找相似的代码。清单 5 中的 CheckStyle 配置文件例子示范了如何使用 StrictDuplicateCode
模块:
清单 5. 使用 CheckStyle 找到至少 10 行重复代码
<module name="StrictDuplicateCode">
<property name="min" value="10"/>
</module>
|
清单 5 中的 min
属性设置了 CheckStyle 将会标记出的最小重复行数,以供查阅。在这样的情况下,它将只指示出那些至少有 10 行类似或重复的代码块。
图 3 展示了自动构建运行后,清单 5 中的模块设置的结果:
图 3. CheckStyle 报告指示代码重复度过高

重构
在清单 6 中,我用了 清单 4中的重复代码,使用了 Pull Up Method重构来降低重复度 —将行为从较大方法提取到一个抽象类方法中:
清单 6. Pull Up Method
...
} finally {
closeDbConnection(rs, stmt, conn);
}
...
|
重复代码是难以避免的。我永远不会建议一个团队去努力实现什么 无重复之类的目标,这是不切实际的。然而,确保代码库中的重复代码不会增多这样的目标是可以实现的。使用诸如 PMD 的 CPD 或 CheckStyle 这样的静态分析工具,您能够将整个分析过程作为自动构建的一部分,持续分析,确定代码重复度高的区域。
回页首
长方法(大类)
味道:长方法(大类)
度量:源代码行数(SLOC)
工具:PMD、JavaNCSS、CheckStyle
重构: Extract Method、Replace Temp with Query、Introduce Parameter Object、Preserve Whole Object、Replace Method with Method Object
味道
我一直在尝试坚持的一条经验法则是将方法限制在 20 行或 20 行以内。当然,这个原则也可能会有例外,但如果我的方法超过 20 行的话,我就会更仔细地去了解它。通常情况下,长方法和条件复杂度是息息相关的。而大类与长方法之间又有着必然的联系。我可以给您展示一个 2200 行的方法,这个方法是我在需要维护的一个项目上发现的。我将整个含有 25000 行的代码的类打印了出来,让我的同事来找出里面的错误。这么说吧,当我把打印出来的代码沿着走廊卷起来的时候,他们就已经同意我的看法了。
清单 7 中高亮显示的部分展示了一个长方法代码味道示例的一小部分:
清单 7. 长方法代码味道
public void saveLedgerInformation() {
...
try {
if (ledger.getId() != null && filename == null) {
getLedgerService().saveLedger(ledger);
} else {
accessFiles().decompressFiles(files, filenames);
}
if (!files.get(0).equals(upload)) {
upload = files.get(0);
filename = filenames.get(0);
}
if (invalidFiles.isUnsupported(filename)) {
setError(fileName, message.getMessage());
} else {
LedgerFile entryFile = accessFiles().add(upload, filename);
if (fileType != null && FileType.valueOf(fileType) != null) {
entryFile.setFileType(FileType.valueOf(fileType));
}
getFileManagementService().saveLedger(ledger, entryFile);
if (!FileStatus.OPENED.equals(entryFile.getFileStatus())) {
getFileManagementService().importLedgerDetails(ledger);
}
if (uncompressedFiles.size() > 1) {
Helper.saveMessage(getText("ledger.file"));
}
if (user.getLastName() != null) {
SearchInfo searchInfo = ServiceLocator.getSearchInfo();
searchInfo.setLedgerInfo(null);
isValid = false;
setDefaultValues();
resetSearchInfo();
if (searchInfoValid && ledger != null) {
isValid = true;
}
}
} catch (InvalidDataFileException e) {
ResultType result = e.getResultType();
for (ValidationMessage message : result.getMessages()) {
setError(fileName, message.getMessage());
}
ledger.setEntryFile(null);
} ...
|
度量
在过去的几年里,SLOC 度量方法被误认为是高效率的象征。尽管我们都知道,并不一定是行数越多越好。但说到复杂度,SLOC 可是一个有用的度量方法。一个方法(或类)的行数越多,将来维护其代码就可能越难。
工具
清单 8 中的脚本为长方法(大类)找到了 SLOC 度量方法:
清单 8. 识别过大的类和方法的 Gant 脚本
target(findLongMethods:"runs static code analysis"){
Ant.mkdir(dir:"target/reports")
Ant.taskdef(name:"pmd", classname:"net.sourceforge.pmd.ant.PMDTask",
classpathref:"build.classpath")
Ant.pmd(shortFilenames:"true"){
codeSizeRules.each{ rfile ->
ruleset(rfile)
}
formatter(type:"xml", tofile:"target/reports/pmd_report.xml")
formatter(type:"html", tofile:"target/reports/pmd_report.html")
fileset(dir:"src"){
include(name:"**/*.java")
}
}
}
|
我又使用了 Gant 访问 Ant API 来执行 Ant 任务。在清单 8 中,我调用 PMD 静态分析工具来搜索代码库中的长方法。PMD(连同 JavaNCSS 与 CheckStyle)也可以用于查找长方法、大类以及其他代码味道。
重构
清单 9 展示了用 Extract Method重构来减少 清单 7中的长方法代码味道的一个例子。将清单 7 的方法中的行为提取到清单 9 的代码中以后,我就可以从清单 7 的 saveLedgerInformation()
方法中调用新建的 isUserValid()
方法了:
清单 9. Extract Method 重构
private boolean isUserValid(User user) {
boolean isValid = false;
if (user.getLastName() != null) {
SearchInfo searchInfo = ServiceLocator.getSearchInfo();
searchInfo.setLedgerInfo(null);
setDefaultValues();
resetSearchInfo();
if (searchInfoValid && ledger != null) {
isValid = true;
}
}
return isValid;
}
|
通常,长方法和大类也暗示着存在其他代码味道,如条件复杂度和重复代码。因此,找到这些长方法和大类也就可以修复其他的问题了。
回页首
太多导入
味道:太多导入
度量:传出耦合(每个类的扇出(fan-out))
工具:CheckStyle
重构:Move Method、Extract Class
味道
太多导入表明一个类过多地依赖于其他的类。您会注意到,由于一个类与很多其他的类耦合得太紧密,修改这个类会导致必须对很多其他的类进行修改,这时就说明这个类存在这种代码味道了。清单 10 中的多个导入就是一个例子:
清单 10. 一个类中的多个导入
import com.integratebutton.search.SiteQuery;
import com.integratebutton.search.OptionsQuery;
import com.integratebutton.search.UserQuery;
import com.integratebutton.search.VisitsQuery;
import com.integratebutton.search.SiteQuery;
import com.integratebutton.search.DateQuery;
import com.integratebutton.search.EvaluationQuery;
import com.integratebutton.search.RangeQuery
import com.integratebutton.search.BuildingQuery;
import com.integratebutton.search.IPQuery;
import com.integratebutton.search.SiteDTO;
import com.integratebutton.search.UrlParams;
import com.integratebutton.search.SiteUtil;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
...
|
度量
找到带有太多责任的类的一个方法就是通过 传出耦合度量方法,亦指 扇出复杂度。扇出复杂度给被分析的类所依附的每一个类赋值 1。
工具
清单 11 展示了一个用 CheckStyle 设置最大扇出复杂度数的例子:
清单 11. 使用 CheckStyle 设置最大扇出复杂度
<module name="ClassFanOutComplexity">
<property name="max" value="10"/>
</module>
|
重构
修复由于太多导入而引发的耦合过紧的方法有很多种。对于诸如 清单 10中那样的代码来说,可用的重构就包括 Move Method重构:将方法从单独的 *Query
类移动到 Java 界面,并定义所有 Query
类必须实现的通用方法。然后再使用 工厂方法模式,这样耦合度就与界面相关联了。
通过使用 Gant 自动构建脚本执行 CheckStyle Ant 任务,我可以搜索代码库,查找过多依赖于其他类的类。当修改这些类中的代码时,就能够实现特定的重构(比如 Move Method)和特定的设计模式,以逐步改进可维护性。
回页首
重构……要尽早且要经常进行
持续集成(Continuous Integration,CI)就是经常集成变更。正如其典型的实现方式一样,每当对项目的版本控制储存库做出一个更改时,运行于独立机器上的自动 CI 服务器就会触发一个自动构建。为了确保 清单 3和 清单 8中的脚本可以在对数据库做出更改时一致地运行,您需要配置一个诸如 Hudsona 这样的 CI 服务器(参见 参考资料)。Hudson 是以 WAR 文件的形式发布的,您可以将它放入任何 Java Web 容器中。
由于 清单 3和 清单 8中的例子使用了 Gant,下面我就简要介绍一下配置 Hudson CI 服务器以运行 Gant 脚本的步骤:
- 在 Hudson 的仪表板上为 Hudson 安装 Gant 插件:首先选择 Manage Hudson,再选择 Manage Plugins,然后选择 Available选项卡。在这个选项卡上选中 Gant 插件复选框,然后单击 Install按钮。
- 重新启动 Web 容器(例如,Tomcat)。
- 选择 Manage Hudson,然后选择 Configure System。在 Gant installation部分,键入一个惟一的名称以及 Groovy 安装到运行 Hudson 的机器上的位置。保存所作更改。
- 返回到仪表板(选择 Hudson链接),选择一个 existing Hudson Job,再选择 Configure,然后单击 Add build step按钮,选择 Invoke Gant script选项。
配置 Hudson,使其运行使用 Gant 编写的自动构建脚本。一旦诸如长方法和条件复杂度这样的代码味道被引入到代码库中,您立刻就会得到与它们相关的度量方法的反馈。
回页首
其他味道与重构
并非所有的味道都有相关的度量方法。但是,静态分析工具能够揭露的味道不止我所示范的这些。表 1 列举了其他的代码味道、工具、以及可能的重构例子:
表 1. 其他味道与重构
味道
工具
重构
死代码 |
PMD |
Remove Code |
临时字段 |
PMD |
Inline Temp |
不一致 / 拘谨(uncommunicative)的名称 |
CheckStyle、PMD |
Rename Method、Rename Field |
长参数列表 |
PMD |
Replace Parameter with Method、Preserve Whole Object、Introduce Parameter Object |
本文提供了一种使代码味道与一种度量方法相关的模式,这种度量方法可以配置为通过自动静态分析工具标记。您可以使用或不使用特定的设计模式来进行重构。这为您提供了一个以可重复的方式一致地查找和修复代码味道的框架。我坚信本文的例子也有助于您使用静态分析工具来查找本文未涉及到代码味道。
<!-- CMA ID: 325238 --><!-- Site ID: 10 --><!-- XSLT stylesheet used to transform this file: dw-document-html-6.0.xsl -->
参考资料
学习
分享到:
相关推荐
3. SonarQube:提供代码质量管理平台,整合多种静态代码分析工具,持续跟踪代码质量,支持集成到 CI/CD 流程。 四、版本控制集成 Eclipse 支持多种版本控制系统,如 Git、SVN、CVS 等。这些插件提供图形化界面,...
这些报告可以集成到持续集成(CI)系统中,作为构建过程的一部分。 5. **社区支持**:checkStyle是一个活跃的开源项目,拥有广泛的社区支持和持续的更新。开发者可以通过社区获取帮助,提出问题,甚至参与到项目的...
同时,随着持续集成和持续部署 (CI/CD) 的普及,重构也会更加紧密地与这些流程相结合,以提高软件开发的整体效率。 总之,重构是一项重要的软件工程活动,它不仅有助于提高代码质量和可维护性,还能促进团队成员...
在实际使用中,通常会将`checkstyle.xml`和`jalopy.xml`集成到持续集成(CI)流程中,例如在构建阶段运行代码检查和格式化。这样可以确保每次提交的代码都符合规定,避免因个人编码习惯差异引发的问题。 为了更好地...
- **Jenkins**: Jenkins是一款开源的CI/CD工具,可自动化构建、测试和部署Java应用。 - **Travis CI/CircleCI**: 这些是云托管的CI服务,可以方便地集成到GitHub等版本控制系统中,实现代码提交即自动测试。 6. *...
持续集成和持续部署(CI/CD)工具,如Jenkins和Travis CI,可以自动化构建、测试和部署流程,确保代码质量并加速开发迭代。 最后,代码质量和风格的统一是提高团队效率的关键。Checkstyle和SonarQube这样的静态代码...
此外,持续集成/持续部署(CI/CD)工具如Jenkins或GitLab CI/CD,可以在每次构建或部署时自动更新配置文件,确保不同环境的一致性。 3. **源码工具**: 源码工具涵盖范围广泛,包括版本控制系统(如Git)、代码编辑...
- **Jenkins**: 自动化构建和测试的持续集成/持续部署(CI/CD)工具。 **质量改进策略** - **迭代改进**: 通过多次迭代, 不断收集反馈, 逐步改善产品质量。 - **敏捷开发**: 采用短周期迭代, 快速响应变化, 提高...
10. **持续集成/持续部署(CI/CD)**: IDE可与Jenkins、Travis CI等工具无缝集成,实现自动化构建和部署流程,加速开发迭代。 总之,Java开发工具2020.3最新版为开发者提供了一个高效、集成的开发环境,集成了各种...
持续集成(CI)是一种软件开发实践,通过频繁地(每天多次)将代码集成到共享的主干分支,并自动化构建和测试,以尽早发现集成错误。 **实施要点**: - 配置CI工具,如Jenkins、GitLab CI。 - 自动触发构建和测试...
在大型项目中,通常会使用持续集成服务器(如Jenkins、Travis CI或CircleCI)来自动化代码统计。在CI配置文件中添加相应的步骤,每次提交时都会触发代码统计,并将结果记录在构建报告中,便于团队成员查阅。 5. **...
10. **持续集成/持续部署(CI/CD)**: 如何将这些工具集成到CI/CD流程中,如Jenkins、Travis CI或GitHub Actions,以确保代码质量和自动部署。 通过深入学习以上知识点,开发者可以更好地理解和利用这些工具来提升...
最后,持续集成/持续部署(CI/CD)工具,如Jenkins和Travis CI,自动化构建、测试和部署过程,加速软件交付。 总的来说,Java开发工具涵盖了从编码、测试、构建到部署的全过程,每个环节都有相应的工具支持。熟练掌握...
- **工具选择**:常见的持续集成工具有Jenkins、Travis CI等,它们支持多种构建和测试任务。 - **自动构建**:每次提交代码后,持续集成系统会自动构建项目,并运行预设的测试脚本。 - **快速反馈**:持续集成系统...
- **Jenkins**:开源的CI/CD服务器,支持自动化构建、测试和部署。 9. **插件** - **Lombok**:简化Java bean类的创建,自动处理getter、setter、equals、hashCode和toString方法。 - **SonarQube**:代码质量...
- 持续集成/持续部署(CI/CD):利用Jenkins或GitLab CI/CD自动化构建和部署过程。 以上是对"osno_test"及相关资源的可能解读,具体细节需参考提供的博文链接或查看实际源码才能得出更准确的结论。
- Jenkins、Travis CI、GitLab CI/CD等工具自动化构建、测试和部署过程。 6. **代码版本控制**: - Git工作流如GitFlow、GitHub Flow指导团队协作开发。 7. **设计模式**: - 学习并理解常见的设计模式,如工厂...
持续集成/持续部署(CI/CD)工具如Jenkins、Travis CI或GitHub Actions可以帮助自动构建、测试和部署项目,确保代码的稳定性和一致性。 总的来说,这个项目可能是一个基于Java的、经过精心设计和优化的新项目,旨在...