追求代码质量 – 用代码怀抱举办重构
副标题#e#
在我上中学的时候,有一位英语西席说:“写作就是重写别人已经 重写过的 对象。” 直到大学,我才真正领略了他这句话的意思。并且,当我自觉地回收 这个实践的时候,就开始喜欢上了写作。我开始为我写的对象孤高。我开始真正在意我的表达方法和要转达的内容。
当我开始开拓人员生涯时,我喜欢阅读有履历的专家编写的技能书籍,并且 想知道为什么他们花这么多时间编写代码。当时,编写代码看起来是件容易的工 作 —— 有些人(老是比我级别高的人)会给我一个问题,而我会用任何可行的 要领办理它。
直到我开始与其他开拓人员相助大型项目,才开始领略我的技术的真正意义 地址。我也就在这个时候起,开始有意识地体贴我编写的代码,甚至体贴起其他 人 编写的代码。此刻我知道了,假如不留意代码质量,那么早晚它们会给我造 成一团乱麻。
我名顿开 的一刻呈此刻 1999 年底,当时我正在阅读 Martin Fowler 那 本影响重大的书 Refactoring: Improving the Design of Existing Code(重 构:改造现有代码的设计,这本书对一系列重构模式举办分类,并由此成立了重 构的民众词汇。在此之前,我一直都在重构我的代码(可能其他人的代码),但 是却不知道本身做的就是重构。此刻,我开始为我编写和重构的代码感想越发自 豪,因为我做的事情正是在促进代码的编写方法并让它们日后更易维护。
什么是重构?
凭据我的概念,重构就是改造已经改造的 代码的行为。实际上,重构是个永 不断止的代码编写进程,它的目标是通过布局的改造而提高代码体的可维护性, 但却不 改变代码的整体行为。重要的是要记着重构与重写 代码明明差异。
重写代码会修改代码的行为甚至合约,而重构保持对外接口稳定。对付重构 要领的客户机来说,看不到区别。工作像以前一样事情,可是事情得更好,主要 是因为加强的可测试性可能明明的机能晋升。
主动和被动重构
那么问题就酿成了“我怎么才气知道什么时候该举办重构呢?” 一段代码的 可维护性是个主观的问题。可是,我们中的大都人城市发明,维护本身编写的代 码要比维护其他人编写的代码容易得多。但在这点上也有争议 —— 在整个职业 生涯中维护本身的代码是最大挑战。没有几个真正的 “代码牛仔” 足够幸运地 可以或许不绝地调动事情,而不必修改其他人的代码。对付我们中的大都人来说,必 须维护其他人的代码恰恰是措施员糊口的一部门。抉择代码是否需要重构的要领 ,凡是是主观的。
可是,也有大概客观地判定代码是否该当重构,岂论是本身的代码照旧别人 的代码。在 这个系列前面的文章中,我先容了如何用代码怀抱客观地测试代码 质量。实际上,可以用代码怀抱很容易地找出大概难以维护的代码。一旦客观地 判定出代码中有问题,那么就可以用利便的重构模式改造它。
老是运行测试用例!
重构别人编写的代码的法门是不要把它弄得更糟。在我重构生涯的早期,学 到的一件事就是在修改一些对象之前 拥有一个测试用例很重要。我是通过费力 的一夜,在我本身整理得很好的重构要领中苦苦寻觅,只为找到一个我不小心破 坏的别人编写的事情正常的代码之后学到这个教导的,不小心粉碎的原因就在于 重构之前没有对应的测试用例。请留意我的告诫,在本身举办重构之前,老是要 运行测试用例!
提取要领模式
Martin Fowler 的书出书之后的几年中,增加了很多新的重构模式分类;但 是,迄今为止最容易进修的模式,也大概是最有效的模式,仍然是提取要领 (Extract Method) 模式。在这个模式中,要领的一个逻辑部门被移除,并被 赋予本身的要领界说。此刻被移走的要领体被新要领的挪用取代,如图 1 的 UML 图所示:
图 1. 提取要领模式实践
提取要领模式提供了两个要害长处:
本来的要领此刻更短了,因此也更容易领略。
移走并放在本身要领中的逻辑表此刻更容易测试。
#p#副标题#e#
低落圈巨大度
在利用的时候,对付被高度圈巨大度值传染的要领来说,提取要领是一剂良 药。您大概会记得,圈巨大度通太过量要领的路径数量;所以,可以认为假如提 取 出个中一些路径,重构要领的整体巨大性会低落。
譬喻,假设在运行了像 PMD 这样的代码阐明东西之后,功效陈诉显示个中一 个类包括的一个要领有较高的圈巨大度值,如图 2 所示:
图 2. 圈巨大度值高达 23!
#p#分页标题#e#
在仔细查察了这个要领之后,发明这个要领过长的原因是利用了太多的条件 逻辑。正如我以前在这个系列中指出的,这会增加要领中发生缺陷的风险。谢天 谢地,updateContent() 要领尚有个测试用例。纵然已经认为这个要领有风险, 测试也会减轻一些 风险。
另一方面,测试已经经心地编写成可以测试 updateContent() 要领中的 23 个路径。实际上,好的法则该当是:该当编写至少 23 个测试。并且,要想编写 一个测试用例,刚好能断绝出要领中的第 18 个条件,那将是极大的挑战!
小就是美
是否真的要测试长要领中的第 18 个条件,是个判定问题。可是,假如逻辑 中包括真实的业务值,就会想到测试它,这个时候就可以看到提取要领模式的作 用了。要把风险降到最小很简朴,只需把条件逻辑解析成更小的片断,然后建设 容易测试的新要领。
譬喻,updateContent() 要领中下面的这小段条件逻辑建设一个状态 String 。如清单 1 所示,逻辑的断绝看起来足够简朴:
清单 1. 条件逻辑成熟到可以举办提取
//...other code above
String retstatus = null;
if ( lastChangedStatus != null && lastChangedStatus.size() > 0 ){
if ( status.getId() == ((IStatus)lastChangedStatus.get (0)).getId() ){
retstatus = "Change in Current status";
}else{
retstatus = "Account Previously Changed in: " +
((IStatus)lastChangedStatus.get(0)).getStatusIdentification ();
}
}else{
retstatus = "No Changes Since Creation";
}
//...more code below
通过把这一小段条件逻辑提取到简捷的新要领中(如清单 2 所示),就做到 了两件事:一,把 updateContent() 要领的整体巨大性低落了 5;二,逻辑的 断绝很完整,可以容易地对它举办测试。
清单 2. 提取要领发生 getStatus
private String getStatus (IStatus status, List lastChangedStatus) {
String retstatus = null;
if ( lastChangedStatus != null && lastChangedStatus.size() > 0 ){
if ( status.getId() == ((IStatus)lastChangedStatus.get (0)).getId() ){
retstatus = "Change in Current status";
}else{
retstatus = "Account Previously Changed in: " +
((IStatus)lastChangedStatus.get(0)).getStatusIdentification ();
}
}else{
retstatus = "No Changes Since Creation";
}
return retstatus;
}
此刻可以把 updateContent() 要领体中的一部门替换成对新建设的 getStatus() 要领的挪用,如清单 3 所示:
清单 3. 挪用 getStatus
//...other code above
String iStatus = getStatus(status, lastChangedStatus);
//...more code below
请记着运行现有的测试,以验证什么都没被粉碎!
测试私有要领
您将留意到在 清单 2 中界说的新 getStatus() 要领被声明为 private。这 在想验证断绝的 要领的行为的时候就形成了一个有趣的挑战。有很多要领可以 办理这个问题:
把要领声明成 public。
把要领声明成 protected,并把测试用例放在同一个包中。
在父类中成立一个内部类,这个内部类是个测试用例。
尚有另一个选择:保存要领现有的声明稳定(即 private),并回收优秀的 JUnit 插件项目来测试它。
PrivateAccessor 类
JUnit 插件项目有一些利便的东西,可以辅佐 JUnit 举办测试。个中最有用 的一个就是 PrivateAccessor 类,它把对 private 要领的测试酿成小菜一碟, 无论选择的测试框架是什么。PrivateAccessor 类对 JUnit 没有显式的依赖, 所以可以把它用于任何测试框架,譬喻 TestNG。
PrivateAccessor 的 API 很简朴 —— 向 invoke() 要领提供要领的名称( 作为 String)和要领对应的参数范例和相关的值(别离在 Class 和 Object 数 组中),就会返回被挪用要领的值。在幕后,PrivateAccessor 类实际上操作 Java 的反射 API 封锁了工具的可会见性。可是请记着,假如虚拟机有定制的安 全性配置,那么这个东西大概无法正确事情。
在清单 4 中,挪用 getStatus() 要领时两个参数值都配置为 null。 invoke() 要领返回一个 Object,所以要转换成 String。还请留意 invoke() 要领声明它要 throws Throwable,必需捕捉异常可能让测试框架处理惩罚它,就像 我做的那样。
清单 4. 测试私有要领
#p#分页标题#e#
public void testGetStatus() throws Throwable{
AccountAction action = new AccountAction();
String value = (String)PrivateAccessor.invoke(action,
"getStatus", new Class[]{IStatus.class, List.class},
new Object[]{null, null});
assertEquals("should be No Changes Since Creation",
"No Changes Since Creation", value);
}
请留意 invoke() 要领被包围成可以接管一个 Object 实例(如清单 4 所示 )或一个 Class(这时期望的 private 要领也是 static 的)。
还请记着,利用反射挪用 private 要了解对生成的功效带来必然水平的懦弱 性。假如有人改变了 getStatus() 要领的名字,以上测试就会失败;可是,如 果常常测试,就可以迅速地举办适当的批改。
竣事语
在抗击圈巨大度时,请记着大部门编写到应用措施中的路径是应用措施的整 体行为所固有的。也就是说,很难显著地淘汰路径的整体数量。重构只是把这些 路径放在更小的代码段中,从而更容易测试。这些小的代码段也更容易维护。