《重构:改善既有代码的设计》(一)

   重构(名词):对软件内部结构的一种调整。目的是在不改变「软件之可察行为」前提下,提高其可理解性,降低其修改成本。

 

  如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地那么做,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

  测试过程中很重要的一部分,就是测试程序对于结果的回报方式。它们要不说 "OK",表示所有新字符串都和参考字符串一样。要不就印出一份失败清单,显示问题字符串的出现行号。这些测试都属于自我检验self-checking)。你必须让测试有能力自我检验,否则就得耗费大把时间来回比对。这会降低你的开发速度。

 

  在软件开发过程中,你可能会花上数小时的时间进行重构,其间可能用上数十个不同的重构准则。 
  我的定义还需要往两方面扩展。首先,重构的目的是使软件更容易被理解和修改。你可以在软件内部做很多修改,但必须对软件「可受观察之外部行为」只造成很小变化,或甚至不造成变化。与之形成对比的是「性能优化」。和重构一样,性能优化通常不会改变组件的行为,除了执行速度),只会改变其内部结构。但是两者出发点不同性能优化往往使代码较难理解,但为了得到所需的性能你不得不那么做。 
  我要强调的第二点是重构不会改变软件「可受观察之行为」— 重构之后软件功能一如以往。任何用户,不论最终用户或程序员,都不知道已有东西发生了变化。译注:「可受观察之行为」其实也包括性能,因为性能是可以被观察的。不过我想我们无需太挑剔这些用词。

  Kent Beck的「两顶帽子」比喻。使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为「添加新功能」和「重构」。添加新功能时,你不应该修改既有代码,只管添加新功能。通过测试,并让测试正常运行。你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程序结构。此时你不应该添加任何测试,除非发现先前遗漏的任何东西。只在绝对必要(用以处理接口变化)时才修改测试。

 

「重构」改进软件设计

  如果没有重构,程序的设计会逐渐腐败变质。当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员愈来愈难通过阅读源码而理解原本设计。重构很像是在整理代码,你所做的就是让所有东西回到应该的位置上。代码结构的流失是累积性的。愈难看出代码所代表的设计意涵,就愈难保护其中设计,于是该设计就腐败得愈快。经常性的重构可以帮助代码维持自己该有的形态。
  同样完成一件事,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码Duplicate Code。这个动作的重要性着眼于未来。代码数量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码数量减少将使未来可能的程序修改动作容易得多。代码愈多,正确的修改就愈困难,因为有更多代码需要理解。你在这儿做了点修改,系统却不如预期那样工作,因为你未曾修改另一处 —— 那儿的代码做着几乎完全一样的事情,只是所处环境略有不同。如果消除重复代码,你就可以确定代码将所有事物和行为都只表述一次,惟一一次,这正是优秀设计的根本。

「重构」使软件更易被理解

  这种编程模式的核心就是「准确说出吾人所欲」。除了计算器外,你的源码还有其它读者。数个月之后可能会有另一位程序员尝试读懂你的代码并做一些修改。我们很容易忘记这第二位读者,但他才是最重要的。计算器是否多花了数个钟头进行编译,又有什么关系呢如果一个程序员花费一周时间来修改某段代码,那才关系重大 — 如果他理解你的代码,这个修改原本只需一小时。
  问题在于,当你努力让程序运转的时候,你不会想到未来出现的那个开发者。是的,是应该改变一下我们的开发节奏,对代码做适当修改,让代码变得更易理解。重构可以帮助我们让代码更易读。一开始进行重构时,你的代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的用途。这种编程模式的核心就是「准确说出你的意思」。
  关于这一点,我没必要表现得如此无私。很多时候那个「未来的开发者」就是我自己。此时重构就显得尤其重要了。

  这种可理解性还有另一方面的作用。我利用重构来协助我理解不熟悉的代码。当我看到不熟悉的码,我必须试着理解其用途。我会真正动手修改代码,让它更好地反映出我的理解,然后重新执行,看它是否仍然正常运作,以此检验我的理解是否正确。 一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋简洁,我发现自己可以看到一些以前看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以在脑子里把这一切都想象出来。Ralph Johnson把这种「早期重构」描述为「擦掉窗户上的污垢,使你看得更远」。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。 

「重构」助你找到bugs
  对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设。

「重构」助你提高编程速度
  良好设计是快速软件开发的根本。事实上拥有良好设计才可能达成快速的开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间愈来愈长,因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补钉,新特性需要更多代码才能实现。恶性循环。
  良好设计是维持软件开发速度的根本。

 

 

何时重构

 

  几乎任何情况下我都反对专门拨出时间进行重构。在我看来,重构本来就不是一件「特别拨出时间做」的事情,重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。

 

三次法则The Rule of Three
  Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是做了;第三次再做类似的事,你就应该重构。
  事不过三,三则重构。(Three strikes and you refactor)

 

是什么让程序如此难以相与?四个原因:
   难以阅读的程序,难以修改。
   逻辑重复duplicated logic的程序,难以修改。
   添加新行为时需修改既有代码者,难以修改。
   带复杂条件逻辑(complex conditional logic)的程序,难以修改。
  因此,我们希望程序 (1) 容易阅读,(2) 所有逻辑都只在惟一地点指定,(3) 新的改动不会危及现有行为,(4) 尽可能简单表达条件逻辑conditional logic。

 

 

  『计算器科学是这样一门科学:它相信所有问题都可以通过多一个间接层indirection来解决。』— Dennis DeBruler

  大多数重构都为程序引入了更多间接层。重构往往把大型对象拆成数个小型对象,把大型函数拆成数个小型函数。 但是,间接层是一柄双刃剑。每次把一个东西分成两份,你就需要多管理一个东西。如果某个对象委托delegate另一对象,后者又委托另一对象,程序会愈加难以阅读。
  间接层的某些价值:
   允许逻辑共享 (To enable sharing of logic )。比如说一个子函数 submethod 在两个不同的地点被调用,或superclass中的某个函数被所有subclasses共享。
   分开解释「意图」和「实现」(To Explain intention and implementation separately)。你可以选择每个class和函数的名字,这给了你一个解释自己意图的机会。class或函数内部则解释实现这个意图的作法。如果class和函数内部又以「更小单元的意图」来编写,你所写的代码就可以「与其结构中的大部分重
要信息沟通」。
   将变化加以隔离(To isolate change)。很可能我在两个不同地点使用同一对象,其中一个地点我想改变对象行为,但如果修改了它,我就要冒「同时影响两处」的风险。为此我做出一个subclass,并在需要修改处引用这个subclass。现在,我可以修改这个subclass而不必承担「无意中影响另一处」的风险。
   将条件逻辑加以编码(To encode conditional logic)。对象有一种匪夷所思的机制:多态消息(polymorphic messages),可以灵活弹性而清晰地表达条件逻辑。只要显式条件逻辑被转化为消息(message)形式,往往便能降低代码的重复、增加清晰度、并提高弹性。

  最常见的变量就是:你如何看待你自己的程序。找出一个缺乏「间接层利益」之处,在不修改现有行为的前提下,为它加入一个间接层。现在你获得了一个更有价值的程序,因为它有较高的质量,让我们在明天(未来)受益。
  请将这种方法与「小心翼翼的事前设计」做个比较。推测性设计总是试图在任何一行代码诞生之前就先让系统拥有所有优秀质量,然后程序员将代码塞进这个强健的骨架中就行了。这个过程的问题在于:太容易猜错。如果运用重构,你就永远不会面临全盘错误的危险。程序自始至终都能保持一致的行为,而你又有
机会为程序添加更多价值不扉的质量。
  还有一种比较少见的重构游戏:找出不值得的间接层,并将它拿掉。这种间接层常以中介函数(intermediate methods)形式出现,也许曾经有过贡献,但芳华已逝。它也可能是个组件,你本来期望在不同地点共享它,或让它表现出多态性(polymorphism),最终却只在一处使用之。如果你找到这种「寄生式间接层」,请把它扔掉。如此一来你会获得一个更有价值的程序,不是因为它取得了更多(先前所列)的四种优秀质量,而是因为它以更少的间接层获得一样多的优秀质量。

 

 

数据库Databases

  在「非对象数据库」(nonobject databases)中,解决这个问题的办法之一就是:在对象模型(object model)和数据库模型(database model)之间插入一个分隔层(separate layer),这就可以隔离两个模型各自的变化。升级某一模型时无需同时升级另一模型,只需升级上述的分隔层即可。这样的分隔层会增加系统复杂度,但可以给你很大的灵活度。如果你同时拥有多个数据库,或如果数据库模型较为复杂使你难以控制,那么即使不进行重构,这分隔层也是很重要的。

 

 

修改接口Changing Interfaces

  该如何面对那些必须修改「已发布接口」的重构手法:

如果重构手法改变了已发布接口(published interface),你必须同时维护新旧两个接口,直到你的所有用户都有时间对这个变化做出反应。幸运的是这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入「重复代码」(duplicated code)的泥淖中难以自拔。你还应该使用Java提供的 deprecation(反对)设施,将旧接口标记为 "deprecated"。这么一来你的调用者就会注意到它了。

 

 

Extract Method(110) 提取两个类的相同方法

Move Method(142)  方法在类间移动

有时候我会保留旧函数,让它调用新函数。如果旧函数是一个public函数,而我又不想修改其他类的接口,这便是一种有用的手法。

Replace Temp with Query(120)  将临时变量替代为查询语句 

它们只在自己所属的函数中有效,所以它们会助长「冗长而复杂」的函数。我喜欢尽量除去这一类临时变量。

临时变量往往引发问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易跟丢它们,尤其在长长的函数中更是如此。当然我这么做也需付出性能上的代价。但是这很容易在之后被优化,而且如果代码有合理的组织和管理,优化就会有很好的效果。

Form Template Method(345)

 

运用多态polymorphism取代条件逻辑

Replace Type Code with State/Strategy(227) 将类型代码 替换为 状态/策略

Replace Conditional with Polymorphism(225) 条件替代为多态


switch语句。在另一个对象的属性attribute基础上运用switch语句并不是什么好主意。如果不得不使用也应该在对象自己的数据上使用,而不是在别人的数据上使用。

 

Pull up field(320)

Extract Class(149)

 

posted @ 2015-07-03 18:24  马小豆包  阅读(445)  评论(0编辑  收藏  举报