重构(Refactoring)就是在不改变软件现有功能的基础上,经过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提升软件的扩展性和维护性。java
也许有人会问,为何不在项目开始时多花些时间把设计作好,而要之后花时间来重构呢?要知道一个完美得能够预见将来任何变化的设计,或一个灵活得能够容 纳任何扩展的设计是不存在的。系统设计人员对即将着手的项目每每只能从大方向予以把控,而没法知道每一个细枝末节,其次永远不变的就是变化,提出需求的用户 每每要在软件成型后,始才开始"品头论足",系统设计人员毕竟不是先知先觉的神仙,功能的变化致使设计的调整再所不免。因此"测试为先,持续重构"做为良 好开发习惯被愈来愈多的人所采纳,测试和重构像黄河的护堤,成为保证软件质量的法宝。程序员
1、为何要重构(Refactoring)算法
在不改变系统功能的状况下,改变系统的实现方式。为何要这么作?投入精力不用来知足客户关心的需求,而是仅仅改变了软件的实现方式,这是不是在浪费客户的投资呢?sql
重构的重要性要从软件的生命周期提及。软件不一样与普通的产品,他是一种智力产品,没有具体的物理形态。一个软件不可能发生物理损耗,界面上的按钮永远不会由于按动次数太多而发生接触不良。那么为何一个软件制造出来之后,却不能永远使用下去呢?
对软件的生命形成威胁的因素只有一个:需求的变动。一个软件老是为解决某种特定的需求而产生,时代在发展,客户的业务也在发生变化。有的需求相对稳定一些,有的需求变化的比较剧烈,还有的需求已经消失了,或者转化成了别的需求。在这种状况下,软件必须相应的改变。
考虑到成本和时间等因素,固然不是全部的需求变化都要在软件系统中实现。可是总的说来,软件要适应需求的变化,以保持本身的生命力。
这就产生了一种糟糕的现象:软件产品最初制造出来,是通过精心的设计,具备良好架构的。可是随着时间的发展、需求的变化,必须不断的修改原有的功能、追 加新的功能,还免不了有一些缺陷须要修改。为了实现变动,不可避免的要违反最初的设计构架。通过一段时间之后,软件的架构就千疮百孔了。bug愈来愈多, 愈来愈难维护,新的需求愈来愈难实现,软件的构架对新的需求渐渐的失去支持能力,而是成为一种制约。最后新需求的开发成本会超过开发一个新的软件的成本, 这就是这个软件系统的生命走到尽头的时候。
重构就可以最大限度的避免这样一种现象。系统发展到必定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行从新的整理。经过重构,不断的调整系统的结构,使系统对于需求的变动始终具备较强的适应能力。数据库
经过重构能够达到如下的目标:编程
·持续偏纠和改进软件设计
重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须作预先的设计,可是没必要是最优的设计,只须要一个合理的解决方案就够了,若是没有重 构、程序设计会逐渐腐败变质,越来越像断线的风筝,脱缰的野马没法控制。重构其实就是整理代码,让全部带着发散倾向的代码回归本位。设计模式
·使代码更易为人所理解
Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出计算机能够理解的程序,只有写出人类容易理解的程序才是优秀的程序员。"对此,笔者感触 很深,有些程序员老是可以快速编写出可运行的代码,但代码中晦涩的命名令人晕眩得须要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?
软件的生命周期每每须要多批程序员来维护,咱们每每忽略了这些后来人。为了使代码容易被他人理解,须要在实现软件功能时作许多额外的事件,如清晰的排版 布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个 很好的态度就是将每一个代码元素像新生儿同样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。
对于那些让人充满迷茫感甚至误导性的命名,须要果决地、大刀阔斧地整容,永远不要手下留情!缓存
·帮助发现隐藏的代码缺陷
孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。笔者常有写下程序后,却发生对本身的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现 这种症状竟然是许多程序员常患的"感冒"。当你也发生这样的情形时,经过重构代码能够加深对原设计的理解,发现其中的问题和隐患,构建出更好的代码。安全
·从长远来看,有助于提升编程效率
当你发现解决一个问题变得异常复杂时,每每不是问题自己形成的,而是你用错了方法,拙劣的设计每每致使臃肿的编码。
改善设计、提升可读性、减小缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来经过重构改进设计,或许会在当前减缓速度,但它带来的后发优点倒是不可低估的。性能优化
2、什么时候着手重构(Refactoring)
新官上任三把火,开始一个全新??、脚不停蹄、加班加点,一支声势浩大的千军万"码"夹裹着程序员激情和扣击键盘的鸣金奋力前行,势如破竹,攻城掠地,直指"黄龙府"。
开发经理是这支浩浩汤汤代码队伍的统帅,他负责这支队伍的命运,当齐恒公站在山顶上看到管仲训练的队伍整齐划一地前进时,他感叹说"我有这样一支军队哪 里还怕没有胜利呢?"。但很遗憾,你手中的这支队伍本来只是散兵游勇,在前进中招兵买马,不断壮大,因此队伍变形在所不免。当开发经理发觉队伍变形时,也 许就是克制住攻克前方山头的诱惑,停下脚步整顿队伍的时候了。
Kent Beck提出了"代码坏味道"的说法,和咱们所提出的"队伍变形"是一样的意思,队伍变形的信号是什么呢?如下列述的代码症状就是"队伍变形"的强烈信号:
·代码中存在重复的代码
中国有118 家整车生产企业,数量几乎等于美、日、欧全部汽车厂家数之和,可是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会致使效率的低效和资源的浪费。
程序代码更是不能搞重复建设,若是同一个类中有相同的代码块,请把它提炼成类的一个独立方法,若是不一样类中具备相同的代码,请把它提炼成一个新类,永远不要重复代码。
·过大的类和过长的方法
过大的类每每是类抽象不合理的结果,类抽象不合理将下降了代码的复用率。方法是类王国中的诸侯国,诸侯国太大势必动摇中央集权。过长的方法因为包含的逻 辑过于复杂,错误机率将直线上升,而可读性则直线降低,类的健壮性很容易被打破。当看到一个过长的方法时,须要想办法将其划分为多个小方法,以便于分而治 之。
·牵一毛而须要动全身的修改
当你发现修改一个小功能,或增长一个小功能时,就引起一次代码地震,也许是你的设计抽象度不够理想,功能代码太过度散所引发的。
·类之间须要过多的通信
A类须要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一块儿,而不该该分家。
·过分耦合的信息链
"计算机是这样一门科学,它相信能够经过添加一个中间层解决任何问题",因此每每中间层会被过多地追加到程序中。若是你在代码中看到须要获取一个信息, 须要一个类的方法调用另外一个类的方法,层层挂接,就象输油管同样节节相连。这每每是由于衔接层太多形成的,须要查看就否有可移除的中间层,或是否能够提供 更直接的调用方法。
·各立山头干革命
若是你发现有两个类或两个方法虽然命名不一样但却拥有类似或相同的功能,你会发现每每是 由于开发团队协调不够形成的。笔者曾经写了一个颇好用的字符串处理类,但由于没有及时通告团队其余人员,后来发现项目中竟然有三个字符串处理类。革命资源 是珍贵的,咱们不该各立山头干革命。
·不完美的设计
在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即 经过Socket向指定的短信平台、语音平台及客户端报警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第 三方系统以报文形式向比对报警系统发送请求,比对报警系统接收并响应这个请求。这又须要用到Socket报文通信,因为原来的设计没有将报文通信模块独立 出来,因此没法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统全部的对外通信都复用这个模块,系统的总体设计也显得更加 合理。
每一个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时惟有敢于更改才是最好的出路。
·缺乏必要的注释
虽然许多软件工程的书籍常提醒程序员须要防止过多注释,但这个担忧好象并无什么必要。每每程序员更感兴趣的是功能实现而非代码注释,由于前者更能带来 成就感,因此代码注释每每不是过多而是过少,过于简单。人的记忆曲线降低的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生"提笔忘字,愈 言且止"的情形。
曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。
3、重构(Refactoring)的难题
学习一种能够大幅提升生产力的新技术时,你老是难以察觉其不适用的场合。一般你在一个特定场景中学习它,这个场景每每是个项目。这种状况下你很难看出什 么会形成这种新技术成效不彰或甚至造成危害。十年前,对象技术(object tech.)的状况也是如此。那时若是有人问我「什么时候不要使用对象」,我很难回答。并不是我认为对象十全十美、没有局限性 — 我最反对这种盲目态度,而是尽管我知道它的好处,但确实不知道其局限性在哪儿。
如今,重构的处境也是如此。咱们知道重构的好处,咱们知道重构能够给咱们的工做带来轻而易举的改变。可是咱们尚未得到足够的经验,咱们还看不到它的局限性。
这 一小节比我但愿的要短。暂且如此吧。随着更多人学会重构技巧,咱们也将对??你应该尝试一下重构,得到它所提供的利益,但在此同时,你也应该时时监控其过 程,注意寻找重构可能引入的问题。请让咱们知道你所遭遇的问题。随着对重构的了解日益增多,咱们将找出更多解决办法,并清楚知道哪些问题是真正难以解决 的。
·数据库(Databases)
「重构」常常出问题的一个领域就是数据库。绝大多数商用程序都与它们背后的 database schema(数据库表格结构)紧密耦合(coupled)在一块儿,这也是database schema如此难以修改的缘由之一。另外一个缘由是数据迁移(migration)。就算你很是当心地将系统分层(layered),将database schema和对象模型(object model)间的依赖降至最低,但database schema的改变仍是让你不得不迁移全部数据,这多是件漫长而烦琐的工做。
在「非对象数据库」(nonobject databases)中,解决这个问题的办法之一就是:在对象模型(object model)和数据库模型(database model)之间插入一个分隔层(separate layer),这就能够隔离两个模型各自的变化。升级某一模型时无需同时升级另外一模型,只需升级上述的分隔层便可。这样的分隔层会增长系统复杂度,但能够 给你很大的灵活度。若是你同时拥有多个数据库,或若是数据库模型较为复杂使你难以控制,那么即便不进行重构,这分隔层也是很重要的。
你无需一开始就插入分隔层,能够在发现对象模型变得不稳定时再产生它。这样你就能够为你的改变找到最好的杠杆效应。
对开发者而言,对象数据库既有帮助也有妨碍。某些面向对象数据库提供不一样版本的对象之间的自动迁移功能,这减小了数据迁移时的工做量,但仍是会损失必定 时间。若是各数据库之间的数据迁移并不是自动进行,你就必须自行完成迁移工做,这个工做量但是很大的。这种状况下你必须更加留神classes内的数据结构 变化。你仍然能够放心将classes的行为转移过去,但转移值域(field)时就必须格外当心。数据还没有被转移前你就得先运用访问函数 (accessors)形成「数据已经转移」的假象。一旦你肯定知道「数据应该在何处」时,就能够一次性地将数据迁移过去。这时唯一须要修改的只有访问函 数(accessors),这也下降了错误风险。
·修改接口(Changing Interfaces)
关于对象,另外一件重要事情是:它们容许你分开修改软件模块的实现(implementation)和接口(interface)。你能够安全地修改某对象内部而不影响他人,但对于接口要特别谨慎 — 若是接口被修改了,任何事情都有可能发生。
一直对重构带来困扰的一件事就是:许多重构手法的确会修改接口。像Rename Method(273)这么简单的重构手法所作的一切就是修改接口。这对极为珍贵的封装概念会带来什么影响呢?
若是某个函数的全部调用动做都在你的控制之下,那么即便修改函数名称也不会有任何问题。哪怕面对一个public函数,只要能取得并修改其全部调用者, 你也能够安心地将这个函数易名。只有当须要修改的接口系被那些「找不到,即便找到也不能修改」的代码使用时,接口的修改才会成为问题。若是状况真是如此, 我就会说:这个接口是个「已发布接口」(published interface)— 比公开接口(public interface)更进一步。接口一旦发行,你就再也没法仅仅修改调用者而可以安全地修改接口了。你须要一个略为复杂的程序。
这个想法改变了咱们的问题。现在的问题是:该如何面对那些必须修改「已发布接口」的重构手法?
简言之,若是重构手法改变了已发布接口(published interface),你必须同时维护新旧两个接口,直到你的全部用户都有时间对这个变化作出反应。幸运的是这不太困难。你一般都有办法把事情组织好,让 旧接口继续工做。请尽可能这么作:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入 「重复代码」(duplicated code)的泥淖中难以自拔。你还应该使用Java提供的 deprecation(反对)设施,将旧接口标记为 "deprecated"。这么一来你的调用者就会注意到它了。
这个过程的一个好例子就是Java容器类(collection classes)。Java 2的新容器取代了原先一些容器。当Java 2容器发布时,JavaSoft花了很大力气来为开发者提供一条顺利迁徙之路。
「保留旧接口」的办法一般可行,但很烦人。起码在一段时间里你必须建造(build)并维护一些额外的函数。它们会使接口变得复杂,使接口难以使用。还 好咱们有另外一个选择:不要发布(publish)接口。固然我不是说要彻底禁止,由于很明显你必得发布一些接口。若是你正在建造供外部使用的APIs,像 Sun所作的那样,确定你必得发布接口。我之因此说尽可能不要发布,是由于我经常看到一些开发团队公开了太多接口。我曾经看到一支三人团队这么工做:每一个人 都向另外两人公开发布接口。这使他们不得不常常来回维护接口,而其实他们本来能够直接进入程序库,径行修改本身管理的那一部分,那会轻松许多。过分强调 「代码拥有权」的团队经常会犯这种错误。发布接口颇有用,但也有代价。因此除非真有必要,别发布接口。这可能意味须要改变你的代码拥有权观念,让每一个人都 能够修改别人的代码,以运应接口的改动。以搭档(成对)编程(Pair Programming)完成这一切一般是个好主意。
不要过早发布(published)接口。请修改你的代码拥有权政策,使重构更顺畅。
Java之中还有一个特别关于「修改接口」的问题:在throws子句中增长一个异常。这并非对签名式(signature)的修改,因此你没法以 delegation(委托手法)隐藏它。但若是用户代码不做出相应修改,编译器不会让它经过。这个问题很难解决。你能够为这个函数选择一个新名 tion(可控式异常)转换成一个unchecked exception(不可控异常)。你也能够拋出一个unchecked异常,不过这样你就会失去检验能力。若是你那么作,你能够警告调用者:这个 unchecked异常往后会变成一个checked异常。这样他们就有时间在本身的代码中加上对此异常的处理。出于这个缘由,我老是喜欢为整个 package定义一个superclass异常(就像java.sql的SQLException),并确保全部public函数只在本身的 throws子句中声明这个异常。这样我就能够为所欲为地定义subclass异常,不会影响调用者,由于调用者永远只知道那个更具通常性的 superclass异常。
·难以经过重构手法完成的设计改动
经过重构,能够排除全部设计错误吗?是否存在某些核心设计决 策,没法以重构手法修改?在这个领域里,咱们的统计数据尚不完整。固然某些状况下咱们能够颇有效地重构,这经常令咱们倍感惊讶,但的确也有难以重构的地 方。好比说在一个项目中,咱们很难(但仍是有可能)将「无安全需求(no security requirements)状况下构造起来的系统」重构为「安全性良好的(good security)系统」。
这种状况下个人办法就是「先 想象重构的状况」。考虑候选设计方案时,我会问本身:将某个设计重构为另外一个设计的难度有多大?若是看上去很简单,我就没必要太担忧选择是否得当,因而我就 会选最简单的设计,哪怕它不能覆盖全部潜在需求也不要紧。但若是预先看不到简单的重构办法,我就会在设计上投入更多力气。不过我发现,这种状况不多出现。
·什么时候不应重构?
有时候你根本不该该重构 — 例如当你应该从新编写全部代码的时候。有时候既有代码实在太混乱,重构它还不如重新写一个来得简单。做出这种决定很困难,我认可我也没有什么好准则能够判断什么时候应该放弃重构。
重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运做。你可能只是试着作点测试,而后就发现代码中尽是错误,根本没法稳定运做。记住,重构以前,代码必须起码可以在大部分状况下正常运做。
一个折衷办法就是:将「大块头软件」重构为「封装良好的小型组件」。而后你就能够逐一对组件做出「重构或重建」的决定。这是一个颇具但愿的办法,但我尚未足够数据,因此也没法写出优秀的指导原则。对于一个重要的古老系统,这确定会是一个很好的方向。
另外,若是项目已近最后期限,你也应该避免重构。在此时机,从重构过程赢得的生产力只有在最后期限事后才能体现出来,而那个时候已经时不我予。Ward Cunningham对此有一个很好的见解。他把未完成的重构工做形容为「债务」。不少公司都须要借债来使本身更有效地运转。可是借债就得付利息,过于复 杂的代码所形成的「维护和扩展的额外开销」就是利息。你能够承受必定程度的利息,但若是利息过高你就会被压垮。把债务管理好是很重要的,你应该随时经过重 构来偿还一部分债务。
若是项目已经很是接近最后期限,你不该该再分心于重构,由于已经没有时间了。不过多个项目经验显示:重构的确可以提升生产力。若是最后你没有足够时间,一般就表示你其实早该进行重构。
4、重构(Refactoring)与设计
「重构」肩负一项特别任务:它和设计彼此互补。初学编程的时候,我埋头就写程序,浑浑噩噩地进行开发。然而很快我便发现,「事先设计」(upfront design)能够助我节省回头工的高昂成本。因而我很快增强这种「预先设计」风格。许多人都把设计看做软件开发的关键环节,而把编程 (programming)看做只是机械式的低级劳动。他们认为设计就像画工程图而编码就像施工。可是你要知道,软件和真实器械有着很大的差别。软件的可 塑性更强,并且彻底是思想产品。正如Alistair Cockburn所说:『有了设计,我能够思考更快,可是其中充满小漏洞。』
有一 种观点认为:重构能够成为「预先设计」的替代品。这意思是你根本没必要作任何设计,只管按照最初想法开始编码,让代码有效运做,而后再将它重构成型。事实上 这种办法真的可行。个人确看过有人这么作,最后得到设计良好的软件。极限编程(Extreme Programming)【Beck, XP】 的支持者极力提倡这种办法。
尽管如上所言,只运用重构也能收到效果,但这并非最有效的途径。是的,即便极限编程(Extreme Programming)爱好者也会进行预先设计。他们会使用CRC卡或相似的东西来检验各类不一样想法,而后才获得第一个可被接受的解决方案,而后才能开 始编码,而后才能重构。关键在于:重构改变了「预先设计」的角色。若是没有重构,你就必须保证「预先设计」正确无误,这个压力太大了。这意味若是未来须要 对原始设计作任何修改,代价都将很是高昂。所以你须要把更多时间和精力放在预先设计上,以免往后修改。
若是你选择重构,问题的重点就转 变了。你仍然作预先设计,可是没必要必定找出正确的解决方案。此刻的你只须要获得一个足够合理的解决方案就够了。你很确定地知道,在实现这个初始解决方案的 时候,你对问题的理解也会逐渐加深,你可能会察觉最佳解决方案和你当初设想的有些不一样。只要有重构这项武器在手,就不成问题,由于重构让往后的修改为本不 再高昂。
这种转变致使一个重要结果:软件设计朝向简化前进了一大步。过去不曾运用重构时,我老是力求获得灵活的解决方案。任何一个需求都让我 提心吊胆地猜疑:在系统寿命期间,这个需求会致使怎样的变化?因为变动设计的代价很是高昂,因此我但愿建造一个足够灵活、足够强固的解决方案,但愿它能承 受我所能预见的全部需求变化。问题在于:要建造一个灵活的解决方案,所需的成本难以估算。灵活的解决方案比简单的解决方案复杂许多,因此最终获得的软件通 常也会更难维护 — 虽然它在我预先设想的??方向上,你也必须理解如何修改设计。若是变化只出如今一两个地方,那不算大问题。然而变化其实可能出如今系统各处。若是在全部可 能的变化出现地点都创建起灵活性,整个系统的复杂度和维护难度都会大大提升。固然,若是最后发现全部这些灵活性都毫无必要,这才是最大的失败。你知道,这 其中确定有些灵活性的确派不上用场,但你却没法预测究竟是哪些派不上用场。为了得到本身想要的灵活性,你不得不加入比实际须要更多的灵活性。
有了重构,你就能够经过一条不一样的途径来应付变化带来的风险。你仍旧须要思考潜在的变化,仍旧须要考虑灵活的解决方案。可是你没必要再逐一实现这些解决方案,而是应该问问本身:『把一个简单的解决方案重构成这个灵活的方案有多大难度?』若是答案是「至关容易」(大多数时候都如此),那么你就只需实现目前的简单方案就好了。
重构能够带来更简单的设计,同时又不损失灵活性,这也下降了设计过程的难度,减轻了设计压力。一旦对重构带来的简单性有更多感觉,你甚至能够没必要再预先 思考前述所谓的灵活方案 — 一旦须要它,你总有足够的信心去重构。是的,当下只管建造可运行的最简化系统,至于灵活而复杂的设计,唔,多数时候你都不会须要它。
劳而无获— Ron Jeffries
Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)的支付过程太慢了。虽然咱们的开发还没结束,这个问题却已经开始困扰咱们,由于它已经拖累了测试速度。
Kent Beck、Martin Fowler和我决定解决这个问题。等待大伙儿会合的时间里,凭着我对这个系统的全盘了解,我开始推测:究竟是什么让系统变慢了?我想到数种可能,而后和 伙伴们谈了几种可能的修改方案。最后,关于「如何让这个系统运行更快」,咱们提出了一些真正的好点子。
而后,咱们拿Kent的量测工具度量了系统性能。我一开始所想的可能性居然全都不是问题肇因。咱们发现:系统把一半时间用来建立「日期」实体(instance)。更有趣的是,全部这些实体都有相同的值。
因而咱们观察日期的建立逻辑,发现有机会将它优化。日期本来是由字符串转换而生,即便无外部输入也是如此。之因此使用字符串转换方式,彻底是为了方便键盘输入。好,也许咱们能够将它优化。
因而咱们观察日期怎样被这个程序运用。咱们发现,不少日期对象都被用来产生「日期区间」实体(instance)。「日期区间」是个对象,由一个起始日期和一个结束日期组成。仔细追踪下去,咱们发现绝大多很多天期区间是空的!
处理日期区间时咱们遵循这样一个规则:若是结束日期在起始日期以前,这个日期区间就该是空的。这是一条很好的规则,彻底符合这个class的须要。采用 此一规则后不久,咱们意识到,建立一个「起始日期在结束日期以后」的日期区间,仍然不算是清晰的代码,因而咱们把这个行为提炼到一个factory method(译注:一个著名的设计模式,见《Design Patterns》),由它专门建立「空的日期区间」。
咱们作了上述修改,使代 码更加清晰,却意外获得了一个惊喜。咱们建立一个固定不变的「空日期区间」对象,并让上述调整后的factory method每次都返回该对象,而再也不每次都建立新对象。这一修改把系统速度提高了几乎一倍,足以让测试速度达到可接受程度。这只花了咱们大约五分钟。
我和团队成员(Kent和Martin谢绝参加)认真推测过:咱们了若指掌的这个程序中可能有什么错误?咱们甚至凭空作了些改进设计,却没有先对系统的真实状况进行量测。
咱们彻底错了。除了一场颇有趣的交谈,咱们什么好事都没作。
教训:哪怕你彻底了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。
5、重构与性能(Performance)
译注:在个人接触经验中,performance一词被不一样的人予以不一样的解释和认知:效率、性能、效能。不一样地区(例如台湾和大陆)的习惯用法亦不相同。本书一遇performance我便译为性能。efficient译为高效,effective译为有效。
关于重构,有一个常被提出的问题:它对程序的性能将形成怎样的影响?为了让软件易于理解,你常会做出一些使程序运行变慢的修改。这是个重要的问题。我并 不同意为了提升设计的纯洁性或把但愿寄托于更快的硬件身上,而忽略了程序性能。已经有不少软件由于速度太慢而被用户拒绝,日益提升的机器速度亦只不过略微 放宽了速度方面的限制而已。可是,换个角度说,虽然重构必然会使软件运行更慢,但它也使软件的性能优化更易进行。除了对性能有严格要求的实时(real time)系统,其它任何状况下「编写快速软件」的秘密就是:首先写出可调(tunable)软件,而后调整它以求得到足够速度。
我看 过三种「编写快速软件」的方法。其中最严格的是「时间预算法」(time budgeting),这一般只用于性能要求极高的实时系统。若是使用这种方法,分解你的设计时就要作好预算,给每一个组件预先分配必定资源 — 包括时间和执行轨迹(footprint)。每一个组件绝对不能超出本身的预算,就算拥有「可在不一样组件之间调度预配时间」的机制也不行。这种方法高度重视 性能,对于心律调节器一类的系统是必须的,由于在这样的系统中迟来的数据就是错误的数据。但对其余类系统(例如我常常开发的企业信息系统)而言,如此追求 高性能就有点过份了。
第二种方法是「持续关切法」(constant attention)。这种方法要求任何程序员在任什么时候间作任何事时,都要设法保持系统的高性能。这种方式很常见,感受上颇有吸引力,但一般不会起太大做 用。任何修改若是是为了提升性能,通??终获得的软件的确更快了,那么这点损失尚有所值,惋惜一般事与愿违,由于性能改善一旦被分散到程序各角落,每次改 善都只不过是从「对程序行为的一个狭隘视角」出发而已。
关于性能,一件颇有趣的事情是:若是你对大多数程序进行分析,你会发现它把大半 时间都耗费在一小半代码身上。若是你一视同仁地优化全部代码,90% 的优化工做都是白费劲儿,由于被你优化的代码有许多可贵被执行起来。你花时间作优化是为了让程序运行更快,但若是由于缺少对程序的清楚认识而花费时间,那 些时间都是被浪费掉了。
第三种性能提高法系利用上述的 "90%" 统计数据。采用这种方法时,你以一种「良好的分解方式」(well-factored manner)来建造本身的程序,不对性能投以任何关切,直至进入性能优化阶段 — 那一般是在开发后期。一旦进入该阶段,你再按照某个特定程序来调整程序性能。
在性能优化阶段中,你首先应该以一个量测工具监控程序的运 行,让它告诉你程序中哪些地方大量消耗时间和空间。这样你就能够找出性能热点(hot spot)所在的一小段代码。而后你应该集中关切这些性能热点,并使用前述「持续关切法」中的优化手段来优化它们。因为你把注意力都集中在热点上,较少的 工做量即可显现较好的成果。即使如此你仍是必须保持谨慎。和重构同样,你应该小幅度进行修改。每走一步都须要编译、测试、再次量测。若是没能提升性能,就 应该撤销这次修改。你应该继续这个「发现热点、去除热点」的过程,直到得到客户满意的性能为止。关于这项技术,McConnell 【McConnell】 为咱们提供了更多信息。
一个被良好分解(well-factored)的程序可从两方面帮助此种优化形式。首先,它 让你有比较充裕的时间进行性能调整(performance tuning),由于有分解良好的代码在手,你就可以更快速地添加功能,也就有更多时间用在性能问题上(准确的量测则保证你把这些时间投资在恰当地点)。 其次,面对分解良好的程序,你在进行性能分析时便有较细的粒度(granularity),因而量测工具把你带入范围较小的程序段落中,而性能的调整也比 较容易些。因为代码更加清晰,所以你可以更好地理解本身的选择,更清楚哪一种调整起关键做用。
我发现重构能够帮助我写出更快的软件。短程看来,重构的确会使软件变慢,但它使优化阶段中的软件性能调整更容易。最终我仍是有赚头。
优化一个薪资系统— Rich Garzaniti
将Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)交给GemStone公司以前,咱们用了至关长的时间开发它。开发过程当中咱们无可避免地发现程序不够 快,因而找了Jim Haungs — GemSmith中的一位好手 — 请他帮咱们优化这个系统。
Jim先用一点时间让他的团队了解系统运做方式,而后以GemStone的ProfMonitor特性编写出一个性能量测工具,将它插入咱们的功能测试中。这个工具能够显示系统产生的对象数量,以及这些对象的诞生点。
令咱们吃惊的是:建立量最大的对象竟是字符串。其中最大的工做量则是反复产生12,000-bytes的字符串。这很特别,由于这字符串实在太大了, 连GemStone惯用的垃圾回收设施都没法处理它。因为它是如此巨大,每当被建立出来,GemStone都会将它分页(paging)至磁盘上。也就是 说字符串的建立居然用上了I/O子系统(译注:分页机制会动用I/O),而每次输出记录时都要产生这样的字符串三次﹗
咱们的第一个解决办法是把一个12,000-bytes字符串缓存(cached)起来,这可解决一大半问题。后来咱们又加以修改,将它直接写入一个file stream,从而避免产生字符串。
解决了「巨大字符串」问题后,Jim的量测工具又发现了一些相似问题,只不过字符串稍微小一些:800-bytes、500-bytes……等等,咱们也都对它们改用file stream,因而问题都解决了。
使用这些技术,咱们稳步提升了系统性能。开发过程当中本来彷佛须要1,000小时以上才能完成的薪资计算,实际运做时只花40小时。一个月后咱们把时间缩短到18小时。正式投入运转时只花12小时。通过一年的运行和改善后,所有计算只需9小时。
咱们的最大改进就是:将程序放在多处理器(multi-processor)计算器上,以多线程(multiple threads)方式运行。最初这个系统并不是按照多线程思惟来设计,但因为代码有良好分解(well factored),因此咱们只花三天时间就让它得以同时运行多个线程了。如今,薪资的计算只需2小时。
在Jim提供工具使咱们得以在实际操做中量度系统性能以前,咱们也猜想过问题所在。但若是只靠猜想,咱们须要很长的时间才能试出真正的解法。真实的量测指出了一个彻底不一样的方向,并大大加快了咱们的进度。
重构(Refactoring)就是在不改变软件现有功能的基础上,经过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提升软件的扩展性和维护性。
也许有人会问,为何不在项目开始时多花些时间把设计作好,而要之后花时间来重构呢?要知道一个完美得能够预见将来任何变化的设计,或一个灵活得能够容 纳任何扩展的设计是不存在的。系统设计人员对即将着手的项目每每只能从大方向予以把控,而没法知道每一个细枝末节,其次永远不变的就是变化,提出需求的用户 每每要在软件成型后,始才开始"品头论足",系统设计人员毕竟不是先知先觉的神仙,功能的变化致使设计的调整再所不免。因此"测试为先,持续重构"做为良 好开发习惯被愈来愈多的人所采纳,测试和重构像黄河的护堤,成为保证软件质量的法宝。
1、为何要重构(Refactoring)
在不改变系统功能的状况下,改变系统的实现方式。为何要这么作?投入精力不用来知足客户关心的需求,而是仅仅改变了软件的实现方式,这是不是在浪费客户的投资呢?
重构的重要性要从软件的生命周期提及。软件不一样与普通的产品,他是一种智力产品,没有具体的物理形态。一个软件不可能发生物理损耗,界面上的按钮永远不会由于按动次数太多而发生接触不良。那么为何一个软件制造出来之后,却不能永远使用下去呢?
对软件的生命形成威胁的因素只有一个:需求的变动。一个软件老是为解决某种特定的需求而产生,时代在发展,客户的业务也在发生变化。有的需求相对稳定一些,有的需求变化的比较剧烈,还有的需求已经消失了,或者转化成了别的需求。在这种状况下,软件必须相应的改变。
考虑到成本和时间等因素,固然不是全部的需求变化都要在软件系统中实现。可是总的说来,软件要适应需求的变化,以保持本身的生命力。
这就产生了一种糟糕的现象:软件产品最初制造出来,是通过精心的设计,具备良好架构的。可是随着时间的发展、需求的变化,必须不断的修改原有的功能、追 加新的功能,还免不了有一些缺陷须要修改。为了实现变动,不可避免的要违反最初的设计构架。通过一段时间之后,软件的架构就千疮百孔了。bug愈来愈多, 愈来愈难维护,新的需求愈来愈难实现,软件的构架对新的需求渐渐的失去支持能力,而是成为一种制约。最后新需求的开发成本会超过开发一个新的软件的成本, 这就是这个软件系统的生命走到尽头的时候。
重构就可以最大限度的避免这样一种现象。系统发展到必定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行从新的整理。经过重构,不断的调整系统的结构,使系统对于需求的变动始终具备较强的适应能力。
经过重构能够达到如下的目标:
·持续偏纠和改进软件设计
重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须作预先的设计,可是没必要是最优的设计,只须要一个合理的解决方案就够了,若是没有重 构、程序设计会逐渐腐败变质,越来越像断线的风筝,脱缰的野马没法控制。重构其实就是整理代码,让全部带着发散倾向的代码回归本位。
·使代码更易为人所理解
Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出计算机能够理解的程序,只有写出人类容易理解的程序才是优秀的程序员。"对此,笔者感触 很深,有些程序员老是可以快速编写出可运行的代码,但代码中晦涩的命名令人晕眩得须要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?
软件的生命周期每每须要多批程序员来维护,咱们每每忽略了这些后来人。为了使代码容易被他人理解,须要在实现软件功能时作许多额外的事件,如清晰的排版 布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个 很好的态度就是将每一个代码元素像新生儿同样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。
对于那些让人充满迷茫感甚至误导性的命名,须要果决地、大刀阔斧地整容,永远不要手下留情!
·帮助发现隐藏的代码缺陷
孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。笔者常有写下程序后,却发生对本身的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现 这种症状竟然是许多程序员常患的"感冒"。当你也发生这样的情形时,经过重构代码能够加深对原设计的理解,发现其中的问题和隐患,构建出更好的代码。
·从长远来看,有助于提升编程效率
当你发现解决一个问题变得异常复杂时,每每不是问题自己形成的,而是你用错了方法,拙劣的设计每每致使臃肿的编码。
改善设计、提升可读性、减小缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来经过重构改进设计,或许会在当前减缓速度,但它带来的后发优点倒是不可低估的。
2、什么时候着手重构(Refactoring)
新官上任三把火,开始一个全新??、脚不停蹄、加班加点,一支声势浩大的千军万"码"夹裹着程序员激情和扣击键盘的鸣金奋力前行,势如破竹,攻城掠地,直指"黄龙府"。
开发经理是这支浩浩汤汤代码队伍的统帅,他负责这支队伍的命运,当齐恒公站在山顶上看到管仲训练的队伍整齐划一地前进时,他感叹说"我有这样一支军队哪 里还怕没有胜利呢?"。但很遗憾,你手中的这支队伍本来只是散兵游勇,在前进中招兵买马,不断壮大,因此队伍变形在所不免。当开发经理发觉队伍变形时,也 许就是克制住攻克前方山头的诱惑,停下脚步整顿队伍的时候了。
Kent Beck提出了"代码坏味道"的说法,和咱们所提出的"队伍变形"是一样的意思,队伍变形的信号是什么呢?如下列述的代码症状就是"队伍变形"的强烈信号:
·代码中存在重复的代码
中国有118 家整车生产企业,数量几乎等于美、日、欧全部汽车厂家数之和,可是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会致使效率的低效和资源的浪费。
程序代码更是不能搞重复建设,若是同一个类中有相同的代码块,请把它提炼成类的一个独立方法,若是不一样类中具备相同的代码,请把它提炼成一个新类,永远不要重复代码。
·过大的类和过长的方法
过大的类每每是类抽象不合理的结果,类抽象不合理将下降了代码的复用率。方法是类王国中的诸侯国,诸侯国太大势必动摇中央集权。过长的方法因为包含的逻 辑过于复杂,错误机率将直线上升,而可读性则直线降低,类的健壮性很容易被打破。当看到一个过长的方法时,须要想办法将其划分为多个小方法,以便于分而治 之。
·牵一毛而须要动全身的修改
当你发现修改一个小功能,或增长一个小功能时,就引起一次代码地震,也许是你的设计抽象度不够理想,功能代码太过度散所引发的。
·类之间须要过多的通信
A类须要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一块儿,而不该该分家。
·过分耦合的信息链
"计算机是这样一门科学,它相信能够经过添加一个中间层解决任何问题",因此每每中间层会被过多地追加到程序中。若是你在代码中看到须要获取一个信息, 须要一个类的方法调用另外一个类的方法,层层挂接,就象输油管同样节节相连。这每每是由于衔接层太多形成的,须要查看就否有可移除的中间层,或是否能够提供 更直接的调用方法。
·各立山头干革命
若是你发现有两个类或两个方法虽然命名不一样但却拥有类似或相同的功能,你会发现每每是 由于开发团队协调不够形成的。笔者曾经写了一个颇好用的字符串处理类,但由于没有及时通告团队其余人员,后来发现项目中竟然有三个字符串处理类。革命资源 是珍贵的,咱们不该各立山头干革命。
·不完美的设计
在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即 经过Socket向指定的短信平台、语音平台及客户端报警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第 三方系统以报文形式向比对报警系统发送请求,比对报警系统接收并响应这个请求。这又须要用到Socket报文通信,因为原来的设计没有将报文通信模块独立 出来,因此没法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统全部的对外通信都复用这个模块,系统的总体设计也显得更加 合理。
每一个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时惟有敢于更改才是最好的出路。
·缺乏必要的注释
虽然许多软件工程的书籍常提醒程序员须要防止过多注释,但这个担忧好象并无什么必要。每每程序员更感兴趣的是功能实现而非代码注释,由于前者更能带来 成就感,因此代码注释每每不是过多而是过少,过于简单。人的记忆曲线降低的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生"提笔忘字,愈 言且止"的情形。
曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。
3、重构(Refactoring)的难题
学习一种能够大幅提升生产力的新技术时,你老是难以察觉其不适用的场合。一般你在一个特定场景中学习它,这个场景每每是个项目。这种状况下你很难看出什 么会形成这种新技术成效不彰或甚至造成危害。十年前,对象技术(object tech.)的状况也是如此。那时若是有人问我「什么时候不要使用对象」,我很难回答。并不是我认为对象十全十美、没有局限性 — 我最反对这种盲目态度,而是尽管我知道它的好处,但确实不知道其局限性在哪儿。
如今,重构的处境也是如此。咱们知道重构的好处,咱们知道重构能够给咱们的工做带来轻而易举的改变。可是咱们尚未得到足够的经验,咱们还看不到它的局限性。
这 一小节比我但愿的要短。暂且如此吧。随着更多人学会重构技巧,咱们也将对??你应该尝试一下重构,得到它所提供的利益,但在此同时,你也应该时时监控其过 程,注意寻找重构可能引入的问题。请让咱们知道你所遭遇的问题。随着对重构的了解日益增多,咱们将找出更多解决办法,并清楚知道哪些问题是真正难以解决 的。
·数据库(Databases)
「重构」常常出问题的一个领域就是数据库。绝大多数商用程序都与它们背后的 database schema(数据库表格结构)紧密耦合(coupled)在一块儿,这也是database schema如此难以修改的缘由之一。另外一个缘由是数据迁移(migration)。就算你很是当心地将系统分层(layered),将database schema和对象模型(object model)间的依赖降至最低,但database schema的改变仍是让你不得不迁移全部数据,这多是件漫长而烦琐的工做。
在「非对象数据库」(nonobject databases)中,解决这个问题的办法之一就是:在对象模型(object model)和数据库模型(database model)之间插入一个分隔层(separate layer),这就能够隔离两个模型各自的变化。升级某一模型时无需同时升级另外一模型,只需升级上述的分隔层便可。这样的分隔层会增长系统复杂度,但能够 给你很大的灵活度。若是你同时拥有多个数据库,或若是数据库模型较为复杂使你难以控制,那么即便不进行重构,这分隔层也是很重要的。
你无需一开始就插入分隔层,能够在发现对象模型变得不稳定时再产生它。这样你就能够为你的改变找到最好的杠杆效应。
对开发者而言,对象数据库既有帮助也有妨碍。某些面向对象数据库提供不一样版本的对象之间的自动迁移功能,这减小了数据迁移时的工做量,但仍是会损失必定 时间。若是各数据库之间的数据迁移并不是自动进行,你就必须自行完成迁移工做,这个工做量但是很大的。这种状况下你必须更加留神classes内的数据结构 变化。你仍然能够放心将classes的行为转移过去,但转移值域(field)时就必须格外当心。数据还没有被转移前你就得先运用访问函数 (accessors)形成「数据已经转移」的假象。一旦你肯定知道「数据应该在何处」时,就能够一次性地将数据迁移过去。这时唯一须要修改的只有访问函 数(accessors),这也下降了错误风险。
·修改接口(Changing Interfaces)
关于对象,另外一件重要事情是:它们容许你分开修改软件模块的实现(implementation)和接口(interface)。你能够安全地修改某对象内部而不影响他人,但对于接口要特别谨慎 — 若是接口被修改了,任何事情都有可能发生。
一直对重构带来困扰的一件事就是:许多重构手法的确会修改接口。像Rename Method(273)这么简单的重构手法所作的一切就是修改接口。这对极为珍贵的封装概念会带来什么影响呢?
若是某个函数的全部调用动做都在你的控制之下,那么即便修改函数名称也不会有任何问题。哪怕面对一个public函数,只要能取得并修改其全部调用者, 你也能够安心地将这个函数易名。只有当须要修改的接口系被那些「找不到,即便找到也不能修改」的代码使用时,接口的修改才会成为问题。若是状况真是如此, 我就会说:这个接口是个「已发布接口」(published interface)— 比公开接口(public interface)更进一步。接口一旦发行,你就再也没法仅仅修改调用者而可以安全地修改接口了。你须要一个略为复杂的程序。
这个想法改变了咱们的问题。现在的问题是:该如何面对那些必须修改「已发布接口」的重构手法?
简言之,若是重构手法改变了已发布接口(published interface),你必须同时维护新旧两个接口,直到你的全部用户都有时间对这个变化作出反应。幸运的是这不太困难。你一般都有办法把事情组织好,让 旧接口继续工做。请尽可能这么作:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入 「重复代码」(duplicated code)的泥淖中难以自拔。你还应该使用Java提供的 deprecation(反对)设施,将旧接口标记为 "deprecated"。这么一来你的调用者就会注意到它了。
这个过程的一个好例子就是Java容器类(collection classes)。Java 2的新容器取代了原先一些容器。当Java 2容器发布时,JavaSoft花了很大力气来为开发者提供一条顺利迁徙之路。
「保留旧接口」的办法一般可行,但很烦人。起码在一段时间里你必须建造(build)并维护一些额外的函数。它们会使接口变得复杂,使接口难以使用。还 好咱们有另外一个选择:不要发布(publish)接口。固然我不是说要彻底禁止,由于很明显你必得发布一些接口。若是你正在建造供外部使用的APIs,像 Sun所作的那样,确定你必得发布接口。我之因此说尽可能不要发布,是由于我经常看到一些开发团队公开了太多接口。我曾经看到一支三人团队这么工做:每一个人 都向另外两人公开发布接口。这使他们不得不常常来回维护接口,而其实他们本来能够直接进入程序库,径行修改本身管理的那一部分,那会轻松许多。过分强调 「代码拥有权」的团队经常会犯这种错误。发布接口颇有用,但也有代价。因此除非真有必要,别发布接口。这可能意味须要改变你的代码拥有权观念,让每一个人都 能够修改别人的代码,以运应接口的改动。以搭档(成对)编程(Pair Programming)完成这一切一般是个好主意。
不要过早发布(published)接口。请修改你的代码拥有权政策,使重构更顺畅。
Java之中还有一个特别关于「修改接口」的问题:在throws子句中增长一个异常。这并非对签名式(signature)的修改,因此你没法以 delegation(委托手法)隐藏它。但若是用户代码不做出相应修改,编译器不会让它经过。这个问题很难解决。你能够为这个函数选择一个新名 tion(可控式异常)转换成一个unchecked exception(不可控异常)。你也能够拋出一个unchecked异常,不过这样你就会失去检验能力。若是你那么作,你能够警告调用者:这个 unchecked异常往后会变成一个checked异常。这样他们就有时间在本身的代码中加上对此异常的处理。出于这个缘由,我老是喜欢为整个 package定义一个superclass异常(就像java.sql的SQLException),并确保全部public函数只在本身的 throws子句中声明这个异常。这样我就能够为所欲为地定义subclass异常,不会影响调用者,由于调用者永远只知道那个更具通常性的 superclass异常。
·难以经过重构手法完成的设计改动
经过重构,能够排除全部设计错误吗?是否存在某些核心设计决 策,没法以重构手法修改?在这个领域里,咱们的统计数据尚不完整。固然某些状况下咱们能够颇有效地重构,这经常令咱们倍感惊讶,但的确也有难以重构的地 方。好比说在一个项目中,咱们很难(但仍是有可能)将「无安全需求(no security requirements)状况下构造起来的系统」重构为「安全性良好的(good security)系统」。
这种状况下个人办法就是「先 想象重构的状况」。考虑候选设计方案时,我会问本身:将某个设计重构为另外一个设计的难度有多大?若是看上去很简单,我就没必要太担忧选择是否得当,因而我就 会选最简单的设计,哪怕它不能覆盖全部潜在需求也不要紧。但若是预先看不到简单的重构办法,我就会在设计上投入更多力气。不过我发现,这种状况不多出现。
·什么时候不应重构?
有时候你根本不该该重构 — 例如当你应该从新编写全部代码的时候。有时候既有代码实在太混乱,重构它还不如重新写一个来得简单。做出这种决定很困难,我认可我也没有什么好准则能够判断什么时候应该放弃重构。
重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运做。你可能只是试着作点测试,而后就发现代码中尽是错误,根本没法稳定运做。记住,重构以前,代码必须起码可以在大部分状况下正常运做。
一个折衷办法就是:将「大块头软件」重构为「封装良好的小型组件」。而后你就能够逐一对组件做出「重构或重建」的决定。这是一个颇具但愿的办法,但我尚未足够数据,因此也没法写出优秀的指导原则。对于一个重要的古老系统,这确定会是一个很好的方向。
另外,若是项目已近最后期限,你也应该避免重构。在此时机,从重构过程赢得的生产力只有在最后期限事后才能体现出来,而那个时候已经时不我予。Ward Cunningham对此有一个很好的见解。他把未完成的重构工做形容为「债务」。不少公司都须要借债来使本身更有效地运转。可是借债就得付利息,过于复 杂的代码所形成的「维护和扩展的额外开销」就是利息。你能够承受必定程度的利息,但若是利息过高你就会被压垮。把债务管理好是很重要的,你应该随时经过重 构来偿还一部分债务。
若是项目已经很是接近最后期限,你不该该再分心于重构,由于已经没有时间了。不过多个项目经验显示:重构的确可以提升生产力。若是最后你没有足够时间,一般就表示你其实早该进行重构。
4、重构(Refactoring)与设计
「重构」肩负一项特别任务:它和设计彼此互补。初学编程的时候,我埋头就写程序,浑浑噩噩地进行开发。然而很快我便发现,「事先设计」(upfront design)能够助我节省回头工的高昂成本。因而我很快增强这种「预先设计」风格。许多人都把设计看做软件开发的关键环节,而把编程 (programming)看做只是机械式的低级劳动。他们认为设计就像画工程图而编码就像施工。可是你要知道,软件和真实器械有着很大的差别。软件的可 塑性更强,并且彻底是思想产品。正如Alistair Cockburn所说:『有了设计,我能够思考更快,可是其中充满小漏洞。』
有一 种观点认为:重构能够成为「预先设计」的替代品。这意思是你根本没必要作任何设计,只管按照最初想法开始编码,让代码有效运做,而后再将它重构成型。事实上 这种办法真的可行。个人确看过有人这么作,最后得到设计良好的软件。极限编程(Extreme Programming)【Beck, XP】 的支持者极力提倡这种办法。
尽管如上所言,只运用重构也能收到效果,但这并非最有效的途径。是的,即便极限编程(Extreme Programming)爱好者也会进行预先设计。他们会使用CRC卡或相似的东西来检验各类不一样想法,而后才获得第一个可被接受的解决方案,而后才能开 始编码,而后才能重构。关键在于:重构改变了「预先设计」的角色。若是没有重构,你就必须保证「预先设计」正确无误,这个压力太大了。这意味若是未来须要 对原始设计作任何修改,代价都将很是高昂。所以你须要把更多时间和精力放在预先设计上,以免往后修改。
若是你选择重构,问题的重点就转 变了。你仍然作预先设计,可是没必要必定找出正确的解决方案。此刻的你只须要获得一个足够合理的解决方案就够了。你很确定地知道,在实现这个初始解决方案的 时候,你对问题的理解也会逐渐加深,你可能会察觉最佳解决方案和你当初设想的有些不一样。只要有重构这项武器在手,就不成问题,由于重构让往后的修改为本不 再高昂。
这种转变致使一个重要结果:软件设计朝向简化前进了一大步。过去不曾运用重构时,我老是力求获得灵活的解决方案。任何一个需求都让我 提心吊胆地猜疑:在系统寿命期间,这个需求会致使怎样的变化?因为变动设计的代价很是高昂,因此我但愿建造一个足够灵活、足够强固的解决方案,但愿它能承 受我所能预见的全部需求变化。问题在于:要建造一个灵活的解决方案,所需的成本难以估算。灵活的解决方案比简单的解决方案复杂许多,因此最终获得的软件通 常也会更难维护 — 虽然它在我预先设想的??方向上,你也必须理解如何修改设计。若是变化只出如今一两个地方,那不算大问题。然而变化其实可能出如今系统各处。若是在全部可 能的变化出现地点都创建起灵活性,整个系统的复杂度和维护难度都会大大提升。固然,若是最后发现全部这些灵活性都毫无必要,这才是最大的失败。你知道,这 其中确定有些灵活性的确派不上用场,但你却没法预测究竟是哪些派不上用场。为了得到本身想要的灵活性,你不得不加入比实际须要更多的灵活性。
有了重构,你就能够经过一条不一样的途径来应付变化带来的风险。你仍旧须要思考潜在的变化,仍旧须要考虑灵活的解决方案。可是你没必要再逐一实现这些解决方案,而是应该问问本身:『把一个简单的解决方案重构成这个灵活的方案有多大难度?』若是答案是「至关容易」(大多数时候都如此),那么你就只需实现目前的简单方案就好了。
重构能够带来更简单的设计,同时又不损失灵活性,这也下降了设计过程的难度,减轻了设计压力。一旦对重构带来的简单性有更多感觉,你甚至能够没必要再预先 思考前述所谓的灵活方案 — 一旦须要它,你总有足够的信心去重构。是的,当下只管建造可运行的最简化系统,至于灵活而复杂的设计,唔,多数时候你都不会须要它。
劳而无获— Ron Jeffries
Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)的支付过程太慢了。虽然咱们的开发还没结束,这个问题却已经开始困扰咱们,由于它已经拖累了测试速度。
Kent Beck、Martin Fowler和我决定解决这个问题。等待大伙儿会合的时间里,凭着我对这个系统的全盘了解,我开始推测:究竟是什么让系统变慢了?我想到数种可能,而后和 伙伴们谈了几种可能的修改方案。最后,关于「如何让这个系统运行更快」,咱们提出了一些真正的好点子。
而后,咱们拿Kent的量测工具度量了系统性能。我一开始所想的可能性居然全都不是问题肇因。咱们发现:系统把一半时间用来建立「日期」实体(instance)。更有趣的是,全部这些实体都有相同的值。
因而咱们观察日期的建立逻辑,发现有机会将它优化。日期本来是由字符串转换而生,即便无外部输入也是如此。之因此使用字符串转换方式,彻底是为了方便键盘输入。好,也许咱们能够将它优化。
因而咱们观察日期怎样被这个程序运用。咱们发现,不少日期对象都被用来产生「日期区间」实体(instance)。「日期区间」是个对象,由一个起始日期和一个结束日期组成。仔细追踪下去,咱们发现绝大多很多天期区间是空的!
处理日期区间时咱们遵循这样一个规则:若是结束日期在起始日期以前,这个日期区间就该是空的。这是一条很好的规则,彻底符合这个class的须要。采用 此一规则后不久,咱们意识到,建立一个「起始日期在结束日期以后」的日期区间,仍然不算是清晰的代码,因而咱们把这个行为提炼到一个factory method(译注:一个著名的设计模式,见《Design Patterns》),由它专门建立「空的日期区间」。
咱们作了上述修改,使代 码更加清晰,却意外获得了一个惊喜。咱们建立一个固定不变的「空日期区间」对象,并让上述调整后的factory method每次都返回该对象,而再也不每次都建立新对象。这一修改把系统速度提高了几乎一倍,足以让测试速度达到可接受程度。这只花了咱们大约五分钟。
我和团队成员(Kent和Martin谢绝参加)认真推测过:咱们了若指掌的这个程序中可能有什么错误?咱们甚至凭空作了些改进设计,却没有先对系统的真实状况进行量测。
咱们彻底错了。除了一场颇有趣的交谈,咱们什么好事都没作。
教训:哪怕你彻底了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。
5、重构与性能(Performance)
译注:在个人接触经验中,performance一词被不一样的人予以不一样的解释和认知:效率、性能、效能。不一样地区(例如台湾和大陆)的习惯用法亦不相同。本书一遇performance我便译为性能。efficient译为高效,effective译为有效。
关于重构,有一个常被提出的问题:它对程序的性能将形成怎样的影响?为了让软件易于理解,你常会做出一些使程序运行变慢的修改。这是个重要的问题。我并 不同意为了提升设计的纯洁性或把但愿寄托于更快的硬件身上,而忽略了程序性能。已经有不少软件由于速度太慢而被用户拒绝,日益提升的机器速度亦只不过略微 放宽了速度方面的限制而已。可是,换个角度说,虽然重构必然会使软件运行更慢,但它也使软件的性能优化更易进行。除了对性能有严格要求的实时(real time)系统,其它任何状况下「编写快速软件」的秘密就是:首先写出可调(tunable)软件,而后调整它以求得到足够速度。
我看 过三种「编写快速软件」的方法。其中最严格的是「时间预算法」(time budgeting),这一般只用于性能要求极高的实时系统。若是使用这种方法,分解你的设计时就要作好预算,给每一个组件预先分配必定资源 — 包括时间和执行轨迹(footprint)。每一个组件绝对不能超出本身的预算,就算拥有「可在不一样组件之间调度预配时间」的机制也不行。这种方法高度重视 性能,对于心律调节器一类的系统是必须的,由于在这样的系统中迟来的数据就是错误的数据。但对其余类系统(例如我常常开发的企业信息系统)而言,如此追求 高性能就有点过份了。
第二种方法是「持续关切法」(constant attention)。这种方法要求任何程序员在任什么时候间作任何事时,都要设法保持系统的高性能。这种方式很常见,感受上颇有吸引力,但一般不会起太大做 用。任何修改若是是为了提升性能,通??终获得的软件的确更快了,那么这点损失尚有所值,惋惜一般事与愿违,由于性能改善一旦被分散到程序各角落,每次改 善都只不过是从「对程序行为的一个狭隘视角」出发而已。
关于性能,一件颇有趣的事情是:若是你对大多数程序进行分析,你会发现它把大半 时间都耗费在一小半代码身上。若是你一视同仁地优化全部代码,90% 的优化工做都是白费劲儿,由于被你优化的代码有许多可贵被执行起来。你花时间作优化是为了让程序运行更快,但若是由于缺少对程序的清楚认识而花费时间,那 些时间都是被浪费掉了。
第三种性能提高法系利用上述的 "90%" 统计数据。采用这种方法时,你以一种「良好的分解方式」(well-factored manner)来建造本身的程序,不对性能投以任何关切,直至进入性能优化阶段 — 那一般是在开发后期。一旦进入该阶段,你再按照某个特定程序来调整程序性能。
在性能优化阶段中,你首先应该以一个量测工具监控程序的运 行,让它告诉你程序中哪些地方大量消耗时间和空间。这样你就能够找出性能热点(hot spot)所在的一小段代码。而后你应该集中关切这些性能热点,并使用前述「持续关切法」中的优化手段来优化它们。因为你把注意力都集中在热点上,较少的 工做量即可显现较好的成果。即使如此你仍是必须保持谨慎。和重构同样,你应该小幅度进行修改。每走一步都须要编译、测试、再次量测。若是没能提升性能,就 应该撤销这次修改。你应该继续这个「发现热点、去除热点」的过程,直到得到客户满意的性能为止。关于这项技术,McConnell 【McConnell】 为咱们提供了更多信息。
一个被良好分解(well-factored)的程序可从两方面帮助此种优化形式。首先,它 让你有比较充裕的时间进行性能调整(performance tuning),由于有分解良好的代码在手,你就可以更快速地添加功能,也就有更多时间用在性能问题上(准确的量测则保证你把这些时间投资在恰当地点)。 其次,面对分解良好的程序,你在进行性能分析时便有较细的粒度(granularity),因而量测工具把你带入范围较小的程序段落中,而性能的调整也比 较容易些。因为代码更加清晰,所以你可以更好地理解本身的选择,更清楚哪一种调整起关键做用。
我发现重构能够帮助我写出更快的软件。短程看来,重构的确会使软件变慢,但它使优化阶段中的软件性能调整更容易。最终我仍是有赚头。
优化一个薪资系统— Rich Garzaniti 将Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)交给GemStone公司以前,咱们用了至关长的时间开发它。开发过程当中咱们无可避免地发现程序不够 快,因而找了Jim Haungs — GemSmith中的一位好手 — 请他帮咱们优化这个系统。 Jim先用一点时间让他的团队了解系统运做方式,而后以GemStone的ProfMonitor特性编写出一个性能量测工具,将它插入咱们的功能测试中。这个工具能够显示系统产生的对象数量,以及这些对象的诞生点。 令咱们吃惊的是:建立量最大的对象竟是字符串。其中最大的工做量则是反复产生12,000-bytes的字符串。这很特别,由于这字符串实在太大了, 连GemStone惯用的垃圾回收设施都没法处理它。因为它是如此巨大,每当被建立出来,GemStone都会将它分页(paging)至磁盘上。也就是 说字符串的建立居然用上了I/O子系统(译注:分页机制会动用I/O),而每次输出记录时都要产生这样的字符串三次﹗ 咱们的第一个解决办法是把一个12,000-bytes字符串缓存(cached)起来,这可解决一大半问题。后来咱们又加以修改,将它直接写入一个file stream,从而避免产生字符串。 解决了「巨大字符串」问题后,Jim的量测工具又发现了一些相似问题,只不过字符串稍微小一些:800-bytes、500-bytes……等等,咱们也都对它们改用file stream,因而问题都解决了。 使用这些技术,咱们稳步提升了系统性能。开发过程当中本来彷佛须要1,000小时以上才能完成的薪资计算,实际运做时只花40小时。一个月后咱们把时间缩短到18小时。正式投入运转时只花12小时。通过一年的运行和改善后,所有计算只需9小时。 咱们的最大改进就是:将程序放在多处理器(multi-processor)计算器上,以多线程(multiple threads)方式运行。最初这个系统并不是按照多线程思惟来设计,但因为代码有良好分解(well factored),因此咱们只花三天时间就让它得以同时运行多个线程了。如今,薪资的计算只需2小时。 在Jim提供工具使咱们得以在实际操做中量度系统性能以前,咱们也猜想过问题所在。但若是只靠猜想,咱们须要很长的时间才能试出真正的解法。真实的量测指出了一个彻底不一样的方向,并大大加快了咱们的进度。