在过去的几年里,我曾看过很多项目的大量源代码,从精美的设计到像是用胶带绑定到一起的代码。我写过新的代码也维护过其他开发人员的源代码。我喜欢编写新的代码,但也喜欢采用一些现有的代码,以某种方法将其简化或将重复的代码提取到一个公共类中。在我早期的工作生涯中,许多人都认为如果不编写新的代码就不会有好的效率。幸好,在 20 世纪 90 年代末,Martin Fowler 编写了 Refactoring一书(参见 参考资料),它使得在不改变外部行为的前提下改进现有代码成为可能。
我在 本系列中所一直推崇的就是 效率:如何减少耗时过程的冗余度,更快速地执行它们。在本文的任务中,我一样推崇这个目标,并且将论述怎样更 有效地执行它们。
重构的一个典型方法是在引入新代码或更改方法时对现有代码做出小小的变动。该技巧面临的挑战在于一个开发团队的开发人员的应用方法不一致,并且很容易错失重构的机会。这也正是我提倡使用静态分析工具识别编码违规的原因所在。有了这些工具,您就能够从总体上了解代码库,并且处于类或方法的级别。幸运的是,在 Java™程序设计中,您可以选择的可免费下载的开源静态分析工具很多:CheckStyle、PMD、FindBugs、JavaNCSS、JDepend 等等。
在本文中,您将学习如何:
我将使用如下的通用格式来检查每一种代码味道:
实质上,这个方法提供了一个找到和修复整个代码库中的代码味道的一个框架。这样您就可以更好地了解到代码库中较危险的部分,然后再做出更改。更好的是,我还会向您展示如何将这个方法集成到自动构建中。
味道:条件复杂度
度量:圈复杂度
工具:CheckStyle、JavaNCSS 以及 PMD
重构:Replace Conditional with Polymorphism、Extract Method
条件复杂度可以以几种不同的方式出现在源代码中。这种代码味道的一个例子就是含有多个条件语句,如 if
、while
或者 for
语句。另一种条件复杂度是以 switch
语句的形式呈现出来的,如清单 1 所示:
... 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 中,我:
BeerType
的 Java 界面 showIngredients()
方法 BeerType
创建了一个实现类 为了使文章保持简洁,我仅为每一个类提供一个方法的实现。显然,创建一个界面的方法可能不只一个。重构能够使代码更易于维护,如 Replace Conditional with Polymorphism 和 Extract Method(本文稍后将会讨论)。
味道:重复代码
度量:代码重复率
工具:CheckStyle、PMD
重构:Extract Method、Pull Up Method、Form Template Method、Substitute Algorithm
重复代码可能在代码库中悄然发生。有时,复制粘贴某些代码要比将该行为泛化到另一个类更简单。但复制粘贴的方法存在一个问题,即它强制将代码复制多份,并且需要维护。而且当复制出的代码发生轻微的变化而引发行为不一致时,就会发生更不易察觉的问题,具体取决于哪个方法在执行该行为。清单 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 中的模块设置的结果:
在清单 6 中,我用了 清单 4中的重复代码,使用了 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 中高亮显示的部分展示了一个长方法代码味道示例的一小部分:
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 度量方法:
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()
方法了:
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 中的多个导入就是一个例子:
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,使其运行使用 Gant 编写的自动构建脚本。一旦诸如长方法和条件复杂度这样的代码味道被引入到代码库中,您立刻就会得到与它们相关的度量方法的反馈。
并非所有的味道都有相关的度量方法。但是,静态分析工具能够揭露的味道不止我所示范的这些。表 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 -->
学习