章节一 重构,第一个案例
重构的第一步
测试过程中很重要的一部分,就是测试程序对于结果的回报方式。它们要不说 “OK”,表示所有新字符串都和参考字符串一样,要不就印出一份失败清单,显示问题字符串的出现行号。这些测试都属于自我检验(self-checking)。是的,你必须让测试有能力自我检验,否则就得耗费大把时间来回比对,这会降低你的开发速度。
TIP:重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验(self-checking)能力。
分解和重组
TIP:重构技术系以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
更改变量名称是值得的行为吗?绝对值得。==好的代码应该清楚表达出自己的功能,变量名称是代码清晰的关键。==
TIP:任何一个傻瓜都能写出计算机可以理解的代码。惟有写出人类容易理解的代码,才是优秀的程序员。
绝大多数情况下,函数应该放在它所使用的数据的所属object(或说class)内
临时变量往往形成问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易失去它们的踪迹,尤其在长长的函数之中更是如此。当然我这么做也需付出性能上的代价
在这点上,是否使用计算来代替临时变量,就要对 计算操作 的开销情况进行分析了。
章节二 重构原则
何谓重构
重构(名词):对软件内部结构的一种调整,目的是在不改变「软件之可察行为」前提下,提高其可理解性,降低修改成本。
重构(动词):使用一系列重构准则(手法〕,在不改变「软件之可察行为」前提 下,调整其结构。
两顶帽子原则
使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为:「添加新功能」和「重构」。添加新功能时,你不应该修改既有代码,只管添加新功能。 通过测试(并让测试正常运行〉,你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程序结构。此时你不应该添加任何测试(除非发现先前遗漏的任何东西),只在绝对必要(用以处理接口变化〕时才修改测试。
何时重构
三次法则
第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是做了;第三次再做类似的事,你就应该重构。
Tip: 事不过三,三则重构(Three strikes and you refactor)
程序有两面价值:「今天可以为你做什么」和「明天可以为你做什么」。大多数时候,我们都只关注自己今天想要程序做什么。不论是修复错误或是添加特性,我们都是为了让程序力更强,让它在今天更有价值。
但是系统当下行为,只是整个故事的一部分,如果没有认清这一点,你无法长期从事编程工作。如果你「为求完成今天任务」而釆取的手法使你不可能在明天完成明天的任务,那么你还是失败。但是,你知道自己今天需要什么,却不一定知道自己明天需要什么。也许你可以猜到明天的需求,也许吧,但肯定还有些事情出乎你的意料。
重构的难题
修改接口 Changing Interfaces
只有当需要修改的接口系被那些「找不到,即使找到也不能修改」的代码使用时,接口的修改才会成为问题。如果情况真是如此,我就会说:这个接口是个「已发布接口」(published interface)——比公开接口(public interface)更进一步。接口一旦发布,你就再也无法仅仅修改调用者而能够安全地修改接口了。 你需要一个略为复杂的程序。
该如何面对那些必须修改「已发布接口」的重构手法?
简言之,如果重构手法改变了已发布接口(published interface〕,你必须同时维护新旧两个接口,直到你的所有用户都有时间对这个变化做出反应。 请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入「重复代码」(duplicated code)的泥淖中难以自拔。你还应该使用Java提供的(deprecation〕设施,将旧接口标记为 “deprecated”。这么一来你的调用者就会注意到它了。
TIP:不要过早发布(publish)接口。请修改你的代码拥有权政策,使重构更顺畅。
Java之中还有一个特别关于「修改接口」的问题:在Throws子句中增加一个异常。这并不是对签名式(signature)的修改,所以你无法以delegation(委托手法)隐 藏它。但如果用户代码不做出相应修改,编译器不会让它通过。这个问题很难解决。你可以为这个函数选择一个新名字,让旧函数调用它,并将这个新增的checked exception(可控式异常〗转换成一个unchecked exception(不可控异常:)。你也可 以拋出一个unchecked异常,不过这样你就会失去检验能力。如果你那么做,你可以警告调用者:这个unchecked异常日后会变成一个checked异常。这样他们就有时间在自己的代码中加上对此异常的处理。出于这个原因,我总是喜欢==为整个package定义一个superclass异常(就像java.sql的SQLException),并确保所有public函数只在自己的throws子句中声明这个异常。== 这样我就可以随心所欲地定义异常,不会影响调用者,因为调用者永远只知道那个更具一般性的superclass异常。
何时不该重构
重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码中满是错误,根本无法稳定运作。记住,==重构之前,代码必须起码能够在大部分情况下正常运作。==
*章节三 内容请见 代码坏味道
构筑测试体系
自我测试的价值
TIP:确保所有测试都完全自动化,让它们检查自己的测试结果。
频繁进行测试 是极限编程( extreme programming XP)的重要一 环。
重构需要测试。如果你想重构,你就必须编写测 试代码。
Junit测试框架
编写测试代码时,我往往一开始先让它们失败。面对既有代码,要不我就修改它(如果我能碰触源码的话),使它测试失败,要不就在assertions中放一个错误期望值,造成测试失败。之所以这么做,是为了向自己证明:测试机制的确可以运行,并且的确测试了它该测试的东西(这就是为什么上面两种作法中我比较喜欢修改待测码的原因)。这可能有些偏执,或许吧,但如果测试代码所测的东西并非你想测的东西,你真的有可能被搞得迷迷糊糊。