|
重構(gòu)(Refactoring)就是在不改變軟件現(xiàn)有功能的基礎(chǔ)上,通過(guò)調(diào)整程序代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理,提高軟件的擴(kuò)展性和維護(hù)性。
也許有人會(huì)問(wèn),為什么不在項(xiàng)目開(kāi)始時(shí)多花些時(shí)間把設(shè)計(jì)做好,而要以后花時(shí)間來(lái)重構(gòu)呢?要知道一個(gè)完美得可以預(yù)見(jiàn)未來(lái)任何變化的設(shè)計(jì),或一個(gè)靈活得可以容納任何擴(kuò)展的設(shè)計(jì)是不存在的。系統(tǒng)設(shè)計(jì)人員對(duì)即將著手的項(xiàng)目往往只能從大方向予以把控,而無(wú)法知道每個(gè)細(xì)枝末節(jié),其次永遠(yuǎn)不變的就是變化,提出需求的用戶往往要在軟件成型后,始才開(kāi)始"品頭論足",系統(tǒng)設(shè)計(jì)人員畢竟不是先知先覺(jué)的神仙,功能的變化導(dǎo)致設(shè)計(jì)的調(diào)整再所難免。所以"測(cè)試為先,持續(xù)重構(gòu)"作為良好開(kāi)發(fā)習(xí)慣被越來(lái)越多的人所采納,測(cè)試和重構(gòu)像黃河的護(hù)堤,成為保證軟件質(zhì)量的法寶。
一、為什么要重構(gòu)(Refactoring)
在不改變系統(tǒng)功能的情況下,改變系統(tǒng)的實(shí)現(xiàn)方式。為什么要這么做?投入精力不用來(lái)滿足客戶關(guān)心的需求,而是僅僅改變了軟件的實(shí)現(xiàn)方式,這是否是在浪費(fèi)客戶的投資呢?
重構(gòu)的重要性要從軟件的生命周期說(shuō)起。軟件不同與普通的產(chǎn)品,他是一種智力產(chǎn)品,沒(méi)有具體的物理形態(tài)。一個(gè)軟件不可能發(fā)生物理?yè)p耗,界面上的按鈕永遠(yuǎn)不會(huì)因?yàn)榘磩?dòng)次數(shù)太多而發(fā)生接觸不良。那么為什么一個(gè)軟件制造出來(lái)以后,卻不能永遠(yuǎn)使用下去呢?
對(duì)軟件的生命造成威脅的因素只有一個(gè):需求的變更。一個(gè)軟件總是為解決某種特定的需求而產(chǎn)生,時(shí)代在發(fā)展,客戶的業(yè)務(wù)也在發(fā)生變化。有的需求相對(duì)穩(wěn)定一些,有的需求變化的比較劇烈,還有的需求已經(jīng)消失了,或者轉(zhuǎn)化成了別的需求。在這種情況下,軟件必須相應(yīng)的改變。
考慮到成本和時(shí)間等因素,當(dāng)然不是所有的需求變化都要在軟件系統(tǒng)中實(shí)現(xiàn)。但是總的說(shuō)來(lái),軟件要適應(yīng)需求的變化,以保持自己的生命力。
這就產(chǎn)生了一種糟糕的現(xiàn)象:軟件產(chǎn)品最初制造出來(lái),是經(jīng)過(guò)精心的設(shè)計(jì),具有良好架構(gòu)的。但是隨著時(shí)間的發(fā)展、需求的變化,必須不斷的修改原有的功能、追加新的功能,還免不了有一些缺陷需要修改。為了實(shí)現(xiàn)變更,不可避免的要違反最初的設(shè)計(jì)構(gòu)架。經(jīng)過(guò)一段時(shí)間以后,軟件的架構(gòu)就千瘡百孔了。bug越來(lái)越多,越來(lái)越難維護(hù),新的需求越來(lái)越難實(shí)現(xiàn),軟件的構(gòu)架對(duì)新的需求漸漸的失去支持能力,而是成為一種制約。最后新需求的開(kāi)發(fā)成本會(huì)超過(guò)開(kāi)發(fā)一個(gè)新的軟件的成本,這就是這個(gè)軟件系統(tǒng)的生命走到盡頭的時(shí)候。
重構(gòu)就能夠最大限度的避免這樣一種現(xiàn)象。系統(tǒng)發(fā)展到一定階段后,使用重構(gòu)的方式,不改變系統(tǒng)的外部功能,只對(duì)內(nèi)部的結(jié)構(gòu)進(jìn)行重新的整理。通過(guò)重構(gòu),不斷的調(diào)整系統(tǒng)的結(jié)構(gòu),使系統(tǒng)對(duì)于需求的變更始終具有較強(qiáng)的適應(yīng)能力。
通過(guò)重構(gòu)可以達(dá)到以下的目標(biāo):
· 持續(xù)偏糾和改進(jìn)軟件設(shè)計(jì)
重構(gòu)和設(shè)計(jì)是相輔相成的,它和設(shè)計(jì)彼此互補(bǔ)。有了重構(gòu),你仍然必須做預(yù)先的設(shè)計(jì),但是不必是最優(yōu)的設(shè)計(jì),只需要一個(gè)合理的解決方案就夠了,如果沒(méi)有重構(gòu)、程序設(shè)計(jì)會(huì)逐漸腐敗變質(zhì),愈來(lái)愈像斷線的風(fēng)箏,脫韁的野馬無(wú)法控制。重構(gòu)其實(shí)就是整理代碼,讓所有帶著發(fā)散傾向的代碼回歸本位。
· 使代碼更易為人所理解
Martin Flower在《重構(gòu)》中有一句經(jīng)典的話:"任何一個(gè)傻瓜都能寫(xiě)出計(jì)算機(jī)可以理解的程序,只有寫(xiě)出人類(lèi)容易理解的程序才是優(yōu)秀的程序員。"對(duì)此,筆者感觸很深,有些程序員總是能夠快速編寫(xiě)出可運(yùn)行的代碼,但代碼中晦澀的命名使人暈眩得需要緊握坐椅扶手,試想一個(gè)新兵到來(lái)接手這樣的代碼他會(huì)不會(huì)想當(dāng)逃兵呢?
軟件的生命周期往往需要多批程序員來(lái)維護(hù),我們往往忽略了這些后來(lái)人。為了使代碼容易被他人理解,需要在實(shí)現(xiàn)軟件功能時(shí)做許多額外的事件,如清晰的排版布局,簡(jiǎn)明扼要的注釋?zhuān)渲忻彩且粋€(gè)重要的方面。一個(gè)很好的辦法就是采用暗喻命名,即以對(duì)象實(shí)現(xiàn)的功能的依據(jù),用形象化或擬人化的手法進(jìn)行命名,一個(gè)很好的態(tài)度就是將每個(gè)代碼元素像新生兒一樣命名,也許筆者有點(diǎn)命名偏執(zhí)狂的傾向,如能榮此雅號(hào),將深以此為幸。
對(duì)于那些讓人充滿迷茫感甚至誤導(dǎo)性的命名,需要果決地、大刀闊斧地整容,永遠(yuǎn)不要手下留情!
· 幫助發(fā)現(xiàn)隱藏的代碼缺陷
孔子說(shuō)過(guò):溫故而知新。重構(gòu)代碼時(shí)逼迫你加深理解原先所寫(xiě)的代碼。筆者常有寫(xiě)下程序后,卻發(fā)生對(duì)自己的程序邏輯不甚理解的情景,曾為此驚悚過(guò),后來(lái)發(fā)現(xiàn)這種癥狀居然是許多程序員常患的"感冒"。當(dāng)你也發(fā)生這樣的情形時(shí),通過(guò)重構(gòu)代碼可以加深對(duì)原設(shè)計(jì)的理解,發(fā)現(xiàn)其中的問(wèn)題和隱患,構(gòu)建出更好的代碼。
· 從長(zhǎng)遠(yuǎn)來(lái)看,有助于提高編程效率
當(dāng)你發(fā)現(xiàn)解決一個(gè)問(wèn)題變得異常復(fù)雜時(shí),往往不是問(wèn)題本身造成的,而是你用錯(cuò)了方法,拙劣的設(shè)計(jì)往往導(dǎo)致臃腫的編碼。
改善設(shè)計(jì)、提高可讀性、減少缺陷都是為了穩(wěn)住陣腳。良好的設(shè)計(jì)是成功的一半,停下來(lái)通過(guò)重構(gòu)改進(jìn)設(shè)計(jì),或許會(huì)在當(dāng)前減緩速度,但它帶來(lái)的后發(fā)優(yōu)勢(shì)卻是不可低估的。
二、何時(shí)著手重構(gòu)(Refactoring)
新官上任三把火,開(kāi)始一個(gè)全新??、腳不停蹄、加班加點(diǎn),一支聲勢(shì)浩大的千軍萬(wàn)"碼"夾裹著程序員激情和扣擊鍵盤(pán)的鳴金奮力前行,勢(shì)如破竹,攻城掠地,直指"黃龍府"。
開(kāi)發(fā)經(jīng)理是這支浩浩湯湯代碼隊(duì)伍的統(tǒng)帥,他負(fù)責(zé)這支隊(duì)伍的命運(yùn),當(dāng)齊恒公站在山頂上看到管仲訓(xùn)練的隊(duì)伍整齊劃一地前進(jìn)時(shí),他感嘆說(shuō)"我有這樣一支軍隊(duì)哪里還怕沒(méi)有勝利呢?"。但很遺憾,你手中的這支隊(duì)伍原本只是散兵游勇,在前進(jìn)中招兵買(mǎi)馬,不斷壯大,所以隊(duì)伍變形在所難免。當(dāng)開(kāi)發(fā)經(jīng)理發(fā)覺(jué)隊(duì)伍變形時(shí),也許就是克制住攻克前方山頭的誘惑,停下腳步整頓隊(duì)伍的時(shí)候了。
Kent Beck提出了"代碼壞味道"的說(shuō)法,和我們所提出的"隊(duì)伍變形"是同樣的意思,隊(duì)伍變形的信號(hào)是什么呢?以下列述的代碼癥狀就是"隊(duì)伍變形"的強(qiáng)烈信號(hào):
· 代碼中存在重復(fù)的代碼
中國(guó)有118 家整車(chē)生產(chǎn)企業(yè),數(shù)量幾乎等于美、日、歐所有汽車(chē)廠家數(shù)之和,但是全國(guó)的年產(chǎn)量卻不及一個(gè)外國(guó)大汽車(chē)公司的產(chǎn)量。重復(fù)建設(shè)只會(huì)導(dǎo)致效率的低效和資源的浪費(fèi)。
程序代碼更是不能搞重復(fù)建設(shè),如果同一個(gè)類(lèi)中有相同的代碼塊,請(qǐng)把它提煉成類(lèi)的一個(gè)獨(dú)立方法,如果不同類(lèi)中具有相同的代碼,請(qǐng)把它提煉成一個(gè)新類(lèi),永遠(yuǎn)不要重復(fù)代碼。
· 過(guò)大的類(lèi)和過(guò)長(zhǎng)的方法
過(guò)大的類(lèi)往往是類(lèi)抽象不合理的結(jié)果,類(lèi)抽象不合理將降低代碼的復(fù)用率。方法是類(lèi)王國(guó)中的諸侯國(guó),諸侯國(guó)太大勢(shì)必動(dòng)搖中央集權(quán)。過(guò)長(zhǎng)的方法由于包含的邏輯過(guò)于復(fù)雜,錯(cuò)誤機(jī)率將直線上升,而可讀性則直線下降,類(lèi)的健壯性很容易被打破。當(dāng)看到一個(gè)過(guò)長(zhǎng)的方法時(shí),需要想辦法將其劃分為多個(gè)小方法,以便于分而治之。
· 牽一毛而需要?jiǎng)尤淼男薷?br /> 當(dāng)你發(fā)現(xiàn)修改一個(gè)小功能,或增加一個(gè)小功能時(shí),就引發(fā)一次代碼地震,也許是你的設(shè)計(jì)抽象度不夠理想,功能代碼太過(guò)分散所引起的。
· 類(lèi)之間需要過(guò)多的通訊
A類(lèi)需要調(diào)用B類(lèi)的過(guò)多方法訪問(wèn)B的內(nèi)部數(shù)據(jù),在關(guān)系上這兩個(gè)類(lèi)顯得有點(diǎn)狎昵,可能這兩個(gè)類(lèi)本應(yīng)該在一起,而不應(yīng)該分家。
· 過(guò)度耦合的信息鏈
"計(jì)算機(jī)是這樣一門(mén)科學(xué),它相信可以通過(guò)添加一個(gè)中間層解決任何問(wèn)題",所以往往中間層會(huì)被過(guò)多地追加到程序中。如果你在代碼中看到需要獲取一個(gè)信息,需要一個(gè)類(lèi)的方法調(diào)用另一個(gè)類(lèi)的方法,層層掛接,就象輸油管一樣節(jié)節(jié)相連。這往往是因?yàn)殂暯訉犹嘣斐傻模枰榭淳头裼锌梢瞥闹虚g層,或是否可以提供更直接的調(diào)用方法。
· 各立山頭干革命
如果你發(fā)現(xiàn)有兩個(gè)類(lèi)或兩個(gè)方法雖然命名不同但卻擁有相似或相同的功能,你會(huì)發(fā)現(xiàn)往往是因?yàn)殚_(kāi)發(fā)團(tuán)隊(duì)協(xié)調(diào)不夠造成的。筆者曾經(jīng)寫(xiě)了一個(gè)頗好用的字符串處理類(lèi),但因?yàn)闆](méi)有及時(shí)通告團(tuán)隊(duì)其他人員,后來(lái)發(fā)現(xiàn)項(xiàng)目中居然有三個(gè)字符串處理類(lèi)。革命資源是珍貴的,我們不應(yīng)各立山頭干革命。
· 不完美的設(shè)計(jì)
在筆者剛完成的一個(gè)比對(duì)報(bào)警項(xiàng)目中,曾安排阿朱開(kāi)發(fā)報(bào)警模塊,即通過(guò)Socket向指定的短信平臺(tái)、語(yǔ)音平臺(tái)及客戶端報(bào)警器插件發(fā)送報(bào)警報(bào)文信息,阿朱出色地完成了這項(xiàng)任務(wù)。后來(lái)用戶又提出了實(shí)時(shí)比對(duì)的需求,即要求第三方系統(tǒng)以報(bào)文形式向比對(duì)報(bào)警系統(tǒng)發(fā)送請(qǐng)求,比對(duì)報(bào)警系統(tǒng)接收并響應(yīng)這個(gè)請(qǐng)求。這又需要用到Socket報(bào)文通訊,由于原來(lái)的設(shè)計(jì)沒(méi)有將報(bào)文通訊模塊獨(dú)立出來(lái),所以無(wú)法復(fù)用阿朱開(kāi)發(fā)的代碼。后來(lái)我及時(shí)調(diào)整了這個(gè)設(shè)計(jì),新增了一個(gè)報(bào)文收發(fā)模塊,使系統(tǒng)所有的對(duì)外通訊都復(fù)用這個(gè)模塊,系統(tǒng)的整體設(shè)計(jì)也顯得更加合理。
每個(gè)系統(tǒng)都或多或少存在不完美的設(shè)計(jì),剛開(kāi)始可能注意不到,到后來(lái)才會(huì)慢慢凸顯出來(lái),此時(shí)唯有勇于更改才是最好的出路。
· 缺少必要的注釋
雖然許多軟件工程的書(shū)籍常提醒程序員需要防止過(guò)多注釋?zhuān)@個(gè)擔(dān)心好象并沒(méi)有什么必要。往往程序員更感興趣的是功能實(shí)現(xiàn)而非代碼注釋?zhuān)驗(yàn)榍罢吒軒?lái)成就感,所以代碼注釋往往不是過(guò)多而是過(guò)少,過(guò)于簡(jiǎn)單。人的記憶曲線下降的坡度是陡得嚇人的,當(dāng)過(guò)了一段時(shí)間后再回頭補(bǔ)注釋時(shí),很容易發(fā)生"提筆忘字,愈言且止"的情形。
曾在網(wǎng)上看到過(guò)微軟的代碼注釋?zhuān)湓敱M程度讓人嘆為觀止,也從中體悟到了微軟成功的一個(gè)經(jīng)驗(yàn)。
三、重構(gòu)(Refactoring)的難題
學(xué)習(xí)一種可以大幅提高生產(chǎn)力的新技術(shù)時(shí),你總是難以察覺(jué)其不適用的場(chǎng)合。通常你在一個(gè)特定場(chǎng)景中學(xué)習(xí)它,這個(gè)場(chǎng)景往往是個(gè)項(xiàng)目。這種情況下你很難看出什么會(huì)造成這種新技術(shù)成效不彰或甚至形成危害。十年前,對(duì)象技術(shù)(object tech.)的情況也是如此。那時(shí)如果有人問(wèn)我「何時(shí)不要使用對(duì)象」,我很難回答。并非我認(rèn)為對(duì)象十全十美、沒(méi)有局限性 — 我最反對(duì)這種盲目態(tài)度,而是盡管我知道它的好處,但確實(shí)不知道其局限性在哪兒。
現(xiàn)在,重構(gòu)的處境也是如此。我們知道重構(gòu)的好處,我們知道重構(gòu)可以給我們的工作帶來(lái)垂手可得的改變。但是我們還沒(méi)有獲得足夠的經(jīng)驗(yàn),我們還看不到它的局限性。
這一小節(jié)比我希望的要短。暫且如此吧。你應(yīng)該嘗試一下重構(gòu),獲得它所提供的利益,但在此同時(shí),你也應(yīng)該時(shí)時(shí)監(jiān)控其過(guò)程,注意尋找重構(gòu)可能引入的問(wèn)題。請(qǐng)讓我們知道你所遭遇的問(wèn)題。隨著對(duì)重構(gòu)的了解日益增多,我們將找出更多解決辦法,并清楚知道哪些問(wèn)題是真正難以解決的。
· 數(shù)據(jù)庫(kù)(Databases)
「重構(gòu)」經(jīng)常出問(wèn)題的一個(gè)領(lǐng)域就是數(shù)據(jù)庫(kù)。絕大多數(shù)商用程序都與它們背后的database schema(數(shù)據(jù)庫(kù)表格結(jié)構(gòu))緊密耦合(coupled)在一起,這也是database schema如此難以修改的原因之一。另一個(gè)原因是數(shù)據(jù)遷移(migration)。就算你非常小心地將系統(tǒng)分層(layered),將database schema和對(duì)象模型(object model)間的依賴降至最低,但database schema的改變還是讓你不得不遷移所有數(shù)據(jù),這可能是件漫長(zhǎng)而煩瑣的工作。
在「非對(duì)象數(shù)據(jù)庫(kù)」(nonobject databases)中,解決這個(gè)問(wèn)題的辦法之一就是:在對(duì)象模型(object model)和數(shù)據(jù)庫(kù)模型(database model)之間插入一個(gè)分隔層(separate layer),這就可以隔離兩個(gè)模型各自的變化。升級(jí)某一模型時(shí)無(wú)需同時(shí)升級(jí)另一模型,只需升級(jí)上述的分隔層即可。這樣的分隔層會(huì)增加系統(tǒng)復(fù)雜度,但可以給你很大的靈活度。如果你同時(shí)擁有多個(gè)數(shù)據(jù)庫(kù),或如果數(shù)據(jù)庫(kù)模型較為復(fù)雜使你難以控制,那么即使不進(jìn)行重構(gòu),這分隔層也是很重要的。
你無(wú)需一開(kāi)始就插入分隔層,可以在發(fā)現(xiàn)對(duì)象模型變得不穩(wěn)定時(shí)再產(chǎn)生它。這樣你就可以為你的改變找到最好的杠桿效應(yīng)。
對(duì)開(kāi)發(fā)者而言,對(duì)象數(shù)據(jù)庫(kù)既有幫助也有妨礙。某些面向?qū)ο髷?shù)據(jù)庫(kù)提供不同版本的對(duì)象之間的自動(dòng)遷移功能,這減少了數(shù)據(jù)遷移時(shí)的工作量,但還是會(huì)損失一定時(shí)間。如果各數(shù)據(jù)庫(kù)之間的數(shù)據(jù)遷移并非自動(dòng)進(jìn)行,你就必須自行完成遷移工作,這個(gè)工作量可是很大的。這種情況下你必須更加留神classes內(nèi)的數(shù)據(jù)結(jié)構(gòu)變化。你仍然可以放心將classes的行為轉(zhuǎn)移過(guò)去,但轉(zhuǎn)移值域(field)時(shí)就必須格外小心。數(shù)據(jù)尚未被轉(zhuǎn)移前你就得先運(yùn)用訪問(wèn)函數(shù)(accessors)造成「數(shù)據(jù)已經(jīng)轉(zhuǎn)移」的假象。一旦你確定知道「數(shù)據(jù)應(yīng)該在何處」時(shí),就可以一次性地將數(shù)據(jù)遷移過(guò)去。這時(shí)惟一需要修改的只有訪問(wèn)函數(shù)(accessors),這也降低了錯(cuò)誤風(fēng)險(xiǎn)。
· 修改接口(Changing Interfaces)
關(guān)于對(duì)象,另一件重要事情是:它們?cè)试S你分開(kāi)修改軟件模塊的實(shí)現(xiàn)(implementation)和接口(interface)。你可以安全地修改某對(duì)象內(nèi)部而不影響他人,但對(duì)于接口要特別謹(jǐn)慎 — 如果接口被修改了,任何事情都有可能發(fā)生。
一直對(duì)重構(gòu)帶來(lái)困擾的一件事就是:許多重構(gòu)手法的確會(huì)修改接口。像Rename Method(273)這么簡(jiǎn)單的重構(gòu)手法所做的一切就是修改接口。這對(duì)極為珍貴的封裝概念會(huì)帶來(lái)什么影響呢?
如果某個(gè)函數(shù)的所有調(diào)用動(dòng)作都在你的控制之下,那么即使修改函數(shù)名稱(chēng)也不會(huì)有任何問(wèn)題。哪怕面對(duì)一個(gè)public函數(shù),只要能取得并修改其所有調(diào)用者,你也可以安心地將這個(gè)函數(shù)易名。只有當(dāng)需要修改的接口是被那些「找不到,即使找到也不能修改」的代碼使用時(shí),接口的修改才會(huì)成為問(wèn)題。如果情況真是如此,我就會(huì)說(shuō):這個(gè)接口是個(gè)「已發(fā)布接口」(published interface)— 比公開(kāi)接口(public interface)更進(jìn)一步。接口一旦發(fā)行,你就再也無(wú)法僅僅修改調(diào)用者而能夠安全地修改接口了。你需要一個(gè)略為復(fù)雜的程序。
這個(gè)想法改變了我們的問(wèn)題。如今的問(wèn)題是:該如何面對(duì)那些必須修改「已發(fā)布接口」的重構(gòu)手法?
簡(jiǎn)言之,如果重構(gòu)手法改變了已發(fā)布接口(published interface),你必須同時(shí)維護(hù)新舊兩個(gè)接口,直到你的所有用戶都有時(shí)間對(duì)這個(gè)變化做出反應(yīng)。幸運(yùn)的是這不太困難。你通常都有辦法把事情組織好,讓舊接口繼續(xù)工作。請(qǐng)盡量這么做:讓舊接口調(diào)用新接口。當(dāng)你要修改某個(gè)函數(shù)名稱(chēng)時(shí),請(qǐng)留下舊函數(shù),讓它調(diào)用新函數(shù)。千萬(wàn)不要拷貝函數(shù)實(shí)現(xiàn)碼,那會(huì)讓你陷入「重復(fù)代碼」(duplicated code)的泥淖中難以自拔。你還應(yīng)該使用Java提供的 deprecation(反對(duì))設(shè)施,將舊接口標(biāo)記為 "deprecated"。這么一來(lái)你的調(diào)用者就會(huì)注意到它了。
這個(gè)過(guò)程的一個(gè)好例子就是Java容器類(lèi)(collection classes)。Java 2的新容器取代了原先一些容器。當(dāng)Java 2容器發(fā)布時(shí),JavaSoft花了很大力氣來(lái)為開(kāi)發(fā)者提供一條順利遷徙之路。
「保留舊接口」的辦法通常可行,但很煩人。起碼在一段時(shí)間里你必須建造(build)并維護(hù)一些額外的函數(shù)。它們會(huì)使接口變得復(fù)雜,使接口難以使用。還好我們有另一個(gè)選擇:不要發(fā)布(publish)接口。當(dāng)然我不是說(shuō)要完全禁止,因?yàn)楹苊黠@你必得發(fā)布一些接口。如果你正在建造供外部使用的APIs,像Sun所做的那樣,肯定你必得發(fā)布接口。我之所以說(shuō)盡量不要發(fā)布,是因?yàn)槲页3?吹揭恍╅_(kāi)發(fā)團(tuán)隊(duì)公開(kāi)了太多接口。我曾經(jīng)看到一支三人團(tuán)隊(duì)這么工作:每個(gè)人都向另外兩人公開(kāi)發(fā)布接口。這使他們不得不經(jīng)常來(lái)回維護(hù)接口,而其實(shí)他們?cè)究梢灾苯舆M(jìn)入程序庫(kù),徑行修改自己管理的那一部分,那會(huì)輕松許多。過(guò)度強(qiáng)調(diào)「代碼擁有權(quán)」的團(tuán)隊(duì)常常會(huì)犯這種錯(cuò)誤。發(fā)布接口很有用,但也有代價(jià)。所以除非真有必要,別發(fā)布接口。這可能意味需要改變你的代碼擁有權(quán)觀念,讓每個(gè)人都可以修改別人的代碼,以適應(yīng)接口的改動(dòng)。以搭檔(成對(duì))編程(Pair Programming)完成這一切通常是個(gè)好主意。
不要過(guò)早發(fā)布(published)接口。請(qǐng)修改你的代碼擁有權(quán)政策,使重構(gòu)更順暢。
Java之中還有一個(gè)特別關(guān)于「修改接口」的問(wèn)題:在throws子句中增加一個(gè)異常。這并不是對(duì)簽名式(signature)的修改,所以你無(wú)法以delegation(委托手法)隱藏它。但如果用戶代碼不作出相應(yīng)修改,編譯器不會(huì)讓它通過(guò)。這個(gè)問(wèn)題很難解決。你可以為這個(gè)函數(shù)選擇一個(gè)新名tion(可控式異常)轉(zhuǎn)換成一個(gè)unchecked exception(不可控異常)。你也可以拋出一個(gè)unchecked異常,不過(guò)這樣你就會(huì)失去檢驗(yàn)?zāi)芰ΑH绻隳敲醋觯憧梢跃嬲{(diào)用者:這個(gè)unchecked異常日后會(huì)變成一個(gè)checked異常。這樣他們就有時(shí)間在自己的代碼中加上對(duì)此異常的處理。出于這個(gè)原因,我總是喜歡為整個(gè)package定義一個(gè)superclass異常(就像Java.sql的SQLException),并確保所有public函數(shù)只在自己的throws子句中聲明這個(gè)異常。這樣我就可以隨心所欲地定義subclass異常,不會(huì)影響調(diào)用者,因?yàn)檎{(diào)用者永遠(yuǎn)只知道那個(gè)更具一般性的superclass異常。
· 難以通過(guò)重構(gòu)手法完成的設(shè)計(jì)改動(dòng)
通過(guò)重構(gòu),可以排除所有設(shè)計(jì)錯(cuò)誤嗎?是否存在某些核心設(shè)計(jì)決策,無(wú)法以重構(gòu)手法修改?在這個(gè)領(lǐng)域里,我們的統(tǒng)計(jì)數(shù)據(jù)尚不完整。當(dāng)然某些情況下我們可以很有效地重構(gòu),這常常令我們倍感驚訝,但的確也有難以重構(gòu)的地方。比如說(shuō)在一個(gè)項(xiàng)目中,我們很難(但還是有可能)將「無(wú)安全需求(no security requirements)情況下構(gòu)造起來(lái)的系統(tǒng)」重構(gòu)為「安全性良好的(good security)系統(tǒng)」。
這種情況下我的辦法就是「先想象重構(gòu)的情況」。考慮候選設(shè)計(jì)方案時(shí),我會(huì)問(wèn)自己:將某個(gè)設(shè)計(jì)重構(gòu)為另一個(gè)設(shè)計(jì)的難度有多大?如果看上去很簡(jiǎn)單,我就不必太擔(dān)心選擇是否得當(dāng),于是我就會(huì)選最簡(jiǎn)單的設(shè)計(jì),哪怕它不能覆蓋所有潛在需求也沒(méi)關(guān)系。但如果預(yù)先看不到簡(jiǎn)單的重構(gòu)辦法,我就會(huì)在設(shè)計(jì)上投入更多力氣。不過(guò)我發(fā)現(xiàn),這種情況很少出現(xiàn)。
· 何時(shí)不該重構(gòu)?
有時(shí)候你根本不應(yīng)該重構(gòu) — 例如當(dāng)你應(yīng)該重新編寫(xiě)所有代碼的時(shí)候。有時(shí)候既有代碼實(shí)在太混亂,重構(gòu)它還不如從新寫(xiě)一個(gè)來(lái)得簡(jiǎn)單。作出這種決定很困難,我承認(rèn)我也沒(méi)有什么好準(zhǔn)則可以判斷何時(shí)應(yīng)該放棄重構(gòu)。
重寫(xiě)(而非重構(gòu))的一個(gè)清楚訊號(hào)就是:現(xiàn)有代碼根本不能正常運(yùn)作。你可能只是試著做點(diǎn)測(cè)試,然后就發(fā)現(xiàn)代碼中滿是錯(cuò)誤,根本無(wú)法穩(wěn)定運(yùn)作。記住,重構(gòu)之前,代碼必須起碼能夠在大部分情況下正常運(yùn)作。
一個(gè)折衷辦法就是:將「大塊頭軟件」重構(gòu)為「封裝良好的小型組件」。然后你就可以逐一對(duì)組件作出「重構(gòu)或重建」的決定。這是一個(gè)頗具希望的辦法,但我還沒(méi)有足夠數(shù)據(jù),所以也無(wú)法寫(xiě)出優(yōu)秀的指導(dǎo)原則。對(duì)于一個(gè)重要的古老系統(tǒng),這肯定會(huì)是一個(gè)很好的方向。
另外,如果項(xiàng)目已近最后期限,你也應(yīng)該避免重構(gòu)。在此時(shí)機(jī),從重構(gòu)過(guò)程贏得的生產(chǎn)力只有在最后期限過(guò)后才能體現(xiàn)出來(lái),而那個(gè)時(shí)候已經(jīng)時(shí)不我予。Ward Cunningham對(duì)此有一個(gè)很好的看法。他把未完成的重構(gòu)工作形容為「?jìng)鶆?wù)」。很多公司都需要借債來(lái)使自己更有效地運(yùn)轉(zhuǎn)。但是借債就得付利息,過(guò)于復(fù)雜的代碼所造成的「維護(hù)和擴(kuò)展的額外開(kāi)銷(xiāo)」就是利息。你可以承受一定程度的利息,但如果利息太高你就會(huì)被壓垮。把債務(wù)管理好是很重要的,你應(yīng)該隨時(shí)通過(guò)重構(gòu)來(lái)償還一部分債務(wù)。
如果項(xiàng)目已經(jīng)非常接近最后期限,你不應(yīng)該再分心于重構(gòu),因?yàn)橐呀?jīng)沒(méi)有時(shí)間了。不過(guò)多個(gè)項(xiàng)目經(jīng)驗(yàn)顯示:重構(gòu)的確能夠提高生產(chǎn)力。如果最后你沒(méi)有足夠時(shí)間,通常就表示你其實(shí)早該進(jìn)行重構(gòu)。
四、重構(gòu)(Refactoring)與設(shè)計(jì)
「重構(gòu)」肩負(fù)一項(xiàng)特別任務(wù):它和設(shè)計(jì)彼此互補(bǔ)。初學(xué)編程的時(shí)候,我埋頭就寫(xiě)程序,渾渾噩噩地進(jìn)行開(kāi)發(fā)。然而很快我便發(fā)現(xiàn),「事先設(shè)計(jì)」(upfront design)可以助我節(jié)省回頭工的高昂成本。于是我很快加強(qiáng)這種「預(yù)先設(shè)計(jì)」風(fēng)格。許多人都把設(shè)計(jì)看作軟件開(kāi)發(fā)的關(guān)鍵環(huán)節(jié),而把編程(programming)看作只是機(jī)械式的低級(jí)勞動(dòng)。他們認(rèn)為設(shè)計(jì)就像畫(huà)工程圖而編碼就像施工。但是你要知道,軟件和真實(shí)器械有著很大的差異。軟件的可塑性更強(qiáng),而且完全是思想產(chǎn)品。正如Alistair Cockburn所說(shuō):『有了設(shè)計(jì),我可以思考更快,但是其中充滿小漏洞。』
有一種觀點(diǎn)認(rèn)為:重構(gòu)可以成為「預(yù)先設(shè)計(jì)」的替代品。這意思是你根本不必做任何設(shè)計(jì),只管按照最初想法開(kāi)始編碼,讓代碼有效運(yùn)作,然后再將它重構(gòu)成型。事實(shí)上這種辦法真的可行。我的確看過(guò)有人這么做,最后獲得設(shè)計(jì)良好的軟件。極限編程(Extreme Programming)【Beck, XP】 的支持者極力提倡這種辦法。
盡管如上所言,只運(yùn)用重構(gòu)也能收到效果,但這并不是最有效的途徑。是的,即使極限編程(Extreme Programming)愛(ài)好者也會(huì)進(jìn)行預(yù)先設(shè)計(jì)。他們會(huì)使用CRC卡或類(lèi)似的東西來(lái)檢驗(yàn)各種不同想法,然后才得到第一個(gè)可被接受的解決方案,然后才能開(kāi)始編碼,然后才能重構(gòu)。關(guān)鍵在于:重構(gòu)改變了「預(yù)先設(shè)計(jì)」的角色。如果沒(méi)有重構(gòu),你就必須保證「預(yù)先設(shè)計(jì)」正確無(wú)誤,這個(gè)壓力太大了。這意味如果將來(lái)需要對(duì)原始設(shè)計(jì)做任何修改,代價(jià)都將非常高昂。因此你需要把更多時(shí)間和精力放在預(yù)先設(shè)計(jì)上,以避免日后修改。
如果你選擇重構(gòu),問(wèn)題的重點(diǎn)就轉(zhuǎn)變了。你仍然做預(yù)先設(shè)計(jì),但是不必一定找出正確的解決方案。此刻的你只需要得到一個(gè)足夠合理的解決方案就夠了。你很肯定地知道,在實(shí)現(xiàn)這個(gè)初始解決方案的時(shí)候,你對(duì)問(wèn)題的理解也會(huì)逐漸加深,你可能會(huì)察覺(jué)最佳解決方案和你當(dāng)初設(shè)想的有些不同。只要有重構(gòu)這項(xiàng)武器在手,就不成問(wèn)題,因?yàn)橹貥?gòu)讓日后的修改成本不再高昂。
這種轉(zhuǎn)變導(dǎo)致一個(gè)重要結(jié)果:軟件設(shè)計(jì)朝向簡(jiǎn)化前進(jìn)了一大步。過(guò)去未曾運(yùn)用重構(gòu)時(shí),我總是力求得到靈活的解決方案。任何一個(gè)需求都讓我提心吊膽地猜疑:在系統(tǒng)壽命期間,這個(gè)需求會(huì)導(dǎo)致怎樣的變化?由于變更設(shè)計(jì)的代價(jià)非常高昂,所以我希望建造一個(gè)足夠靈活、足夠強(qiáng)固的解決方案,希望它能承受我所能預(yù)見(jiàn)的所有需求變化。問(wèn)題在于:要建造一個(gè)靈活的解決方案,所需的成本難以估算。靈活的解決方案比簡(jiǎn)單的解決方案復(fù)雜許多,所以最終得到的軟件通常也會(huì)更難維護(hù) — 雖然它在我預(yù)先設(shè)想的方向上,你也必須理解如何修改設(shè)計(jì)。如果變化只出現(xiàn)在一兩個(gè)地方,那不算大問(wèn)題。然而變化其實(shí)可能出現(xiàn)在系統(tǒng)各處。如果在所有可能的變化出現(xiàn)地點(diǎn)都建立起靈活性,整個(gè)系統(tǒng)的復(fù)雜度和維護(hù)難度都會(huì)大大提高。當(dāng)然,如果最后發(fā)現(xiàn)所有這些靈活性都毫無(wú)必要,這才是最大的失敗。你知道,這其中肯定有些靈活性的確派不上用場(chǎng),但你卻無(wú)法預(yù)測(cè)到底是哪些派不上用場(chǎng)。為了獲得自己想要的靈活性,你不得不加入比實(shí)際需要更多的靈活性。
有了重構(gòu),你就可以通過(guò)一條不同的途徑來(lái)應(yīng)付變化帶來(lái)的風(fēng)險(xiǎn)。你仍舊需要思考潛在的變化,仍舊需要考慮靈活的解決方案。但是你不必再逐一實(shí)現(xiàn)這些解決方案,而是應(yīng)該問(wèn)問(wèn)自己:『把一個(gè)簡(jiǎn)單的解決方案重構(gòu)成這個(gè)靈活的方案有多大難度?』如果答案是「相當(dāng)容易」(大多數(shù)時(shí)候都如此),那么你就只需實(shí)現(xiàn)目前的簡(jiǎn)單方案就行了。
重構(gòu)可以帶來(lái)更簡(jiǎn)單的設(shè)計(jì),同時(shí)又不損失靈活性,這也降低了設(shè)計(jì)過(guò)程的難度,減輕了設(shè)計(jì)壓力。一旦對(duì)重構(gòu)帶來(lái)的簡(jiǎn)單性有更多感受,你甚至可以不必再預(yù)先思考前述所謂的靈活方案 — 一旦需要它,你總有足夠的信心去重構(gòu)。是的,當(dāng)下只管建造可運(yùn)行的最簡(jiǎn)化系統(tǒng),至于靈活而復(fù)雜的設(shè)計(jì),唔,多數(shù)時(shí)候你都不會(huì)需要它。
勞而無(wú)獲— Ron Jeffries
Chrysler Comprehensive Compensation(克萊斯勒綜合薪資系統(tǒng))的支付過(guò)程太慢了。雖然我們的開(kāi)發(fā)還沒(méi)結(jié)束,這個(gè)問(wèn)題卻已經(jīng)開(kāi)始困擾我們,因?yàn)樗呀?jīng)拖累了測(cè)試速度。
Kent Beck、Martin Fowler和我決定解決這個(gè)問(wèn)題。等待大伙兒會(huì)合的時(shí)間里,憑著我對(duì)這個(gè)系統(tǒng)的全盤(pán)了解,我開(kāi)始推測(cè):到底是什么讓系統(tǒng)變慢了?我想到數(shù)種可能,然后和伙伴們談了幾種可能的修改方案。最后,關(guān)于「如何讓這個(gè)系統(tǒng)運(yùn)行更快」,我們提出了一些真正的好點(diǎn)子。
然后,我們拿Kent的量測(cè)工具度量了系統(tǒng)性能。我一開(kāi)始所想的可能性竟然全都不是問(wèn)題肇因。我們發(fā)現(xiàn):系統(tǒng)把一半時(shí)間用來(lái)創(chuàng)建「日期」實(shí)體(instance)。更有趣的是,所有這些實(shí)體都有相同的值。
于是我們觀察日期的創(chuàng)建邏輯,發(fā)現(xiàn)有機(jī)會(huì)將它優(yōu)化。日期原本是由字符串轉(zhuǎn)換而生,即使無(wú)外部輸入也是如此。之所以使用字符串轉(zhuǎn)換方式,完全是為了方便鍵盤(pán)輸入。好,也許我們可以將它優(yōu)化。
于是我們觀察日期怎樣被這個(gè)程序運(yùn)用。我們發(fā)現(xiàn),很多日期對(duì)象都被用來(lái)產(chǎn)生「日期區(qū)間」實(shí)體(instance)。「日期區(qū)間」是個(gè)對(duì)象,由一個(gè)起始日期和一個(gè)結(jié)束日期組成。仔細(xì)追蹤下去,我們發(fā)現(xiàn)絕大多數(shù)日期區(qū)間是空的!
處理日期區(qū)間時(shí)我們遵循這樣一個(gè)規(guī)則:如果結(jié)束日期在起始日期之前,這個(gè)日期區(qū)間就該是空的。這是一條很好的規(guī)則,完全符合這個(gè)class的需要。采用此一規(guī)則后不久,我們意識(shí)到,創(chuàng)建一個(gè)「起始日期在結(jié)束日期之后」的日期區(qū)間,仍然不算是清晰的代碼,于是我們把這個(gè)行為提煉到一個(gè)factory method(譯注:一個(gè)著名的設(shè)計(jì)模式,見(jiàn)《Design Patterns》),由它專(zhuān)門(mén)創(chuàng)建「空的日期區(qū)間」。
我們做了上述修改,使代碼更加清晰,卻意外得到了一個(gè)驚喜。我們創(chuàng)建一個(gè)固定不變的「空日期區(qū)間」對(duì)象,并讓上述調(diào)整后的factory method每次都返回該對(duì)象,而不再每次都創(chuàng)建新對(duì)象。這一修改把系統(tǒng)速度提升了幾乎一倍,足以讓測(cè)試速度達(dá)到可接受程度。這只花了我們大約五分鐘。
我和團(tuán)隊(duì)成員(Kent和Martin謝絕參加)認(rèn)真推測(cè)過(guò):我們了若指掌的這個(gè)程序中可能有什么錯(cuò)誤?我們甚至憑空做了些改進(jìn)設(shè)計(jì),卻沒(méi)有先對(duì)系統(tǒng)的真實(shí)情況進(jìn)行量測(cè)。
我們完全錯(cuò)了。除了一場(chǎng)很有趣的交談,我們什么好事都沒(méi)做。
教訓(xùn):哪怕你完全了解系統(tǒng),也請(qǐng)實(shí)際量測(cè)它的性能,不要臆測(cè)。臆測(cè)會(huì)讓你學(xué)到一些東西,但十有八九你是錯(cuò)的。
五、重構(gòu)與性能(Performance)
譯注:在我的接觸經(jīng)驗(yàn)中,performance一詞被不同的人予以不同的解釋和認(rèn)知:效率、性能、效能。不同地區(qū)(例如臺(tái)灣和大陸)的習(xí)慣用法亦不相同。本書(shū)一遇performance我便譯為性能。efficient譯為高效,effective譯為有效。
關(guān)于重構(gòu),有一個(gè)常被提出的問(wèn)題:它對(duì)程序的性能將造成怎樣的影響?為了讓軟件易于理解,你常會(huì)作出一些使程序運(yùn)行變慢的修改。這是個(gè)重要的問(wèn)題。我并不贊成為了提高設(shè)計(jì)的純潔性或把希望寄托于更快的硬件身上,而忽略了程序性能。已經(jīng)有很多軟件因?yàn)樗俣忍挥脩艟芙^,日益提高的機(jī)器速度亦只不過(guò)略微放寬了速度方面的限制而已。但是,換個(gè)角度說(shuō),雖然重構(gòu)必然會(huì)使軟件運(yùn)行更慢,但它也使軟件的性能優(yōu)化更易進(jìn)行。除了對(duì)性能有嚴(yán)格要求的實(shí)時(shí)(real time)系統(tǒng),其它任何情況下「編寫(xiě)快速軟件」的秘密就是:首先寫(xiě)出可調(diào)(tunable)軟件,然后調(diào)整它以求獲得足夠速度。
我看過(guò)三種「編寫(xiě)快速軟件」的方法。其中最嚴(yán)格的是「時(shí)間預(yù)算法」(time budgeting),這通常只用于性能要求極高的實(shí)時(shí)系統(tǒng)。如果使用這種方法,分解你的設(shè)計(jì)時(shí)就要做好預(yù)算,給每個(gè)組件預(yù)先分配一定資源 — 包括時(shí)間和執(zhí)行軌跡(footprint)。每個(gè)組件絕對(duì)不能超出自己的預(yù)算,就算擁有「可在不同組件之間調(diào)度預(yù)配時(shí)間」的機(jī)制也不行。這種方法高度重視性能,對(duì)于心律調(diào)節(jié)器一類(lèi)的系統(tǒng)是必須的,因?yàn)樵谶@樣的系統(tǒng)中遲來(lái)的數(shù)據(jù)就是錯(cuò)誤的數(shù)據(jù)。但對(duì)其他類(lèi)系統(tǒng)(例如我經(jīng)常開(kāi)發(fā)的企業(yè)信息系統(tǒng))而言,如此追求高性能就有點(diǎn)過(guò)份了。
第二種方法是「持續(xù)關(guān)切法」(constant attention)。這種方法要求任何程序員在任何時(shí)間做任何事時(shí),都要設(shè)法保持系統(tǒng)的高性能。這種方式很常見(jiàn),感覺(jué)上很有吸引力,但通常不會(huì)起太大作用。任何修改如果是為了提高性能,最終得到的軟件的確更快了,那么這點(diǎn)損失尚有所值,可惜通常事與愿違,因?yàn)樾阅芨纳埔坏┍环稚⒌匠绦蚋鹘锹洌看胃纳贫贾徊贿^(guò)是從「對(duì)程序行為的一個(gè)狹隘視角」出發(fā)而已。
關(guān)于性能,一件很有趣的事情是:如果你對(duì)大多數(shù)程序進(jìn)行分析,你會(huì)發(fā)現(xiàn)它把大半時(shí)間都耗費(fèi)在一小半代碼身上。如果你一視同仁地優(yōu)化所有代碼,90% 的優(yōu)化工作都是白費(fèi)勁兒,因?yàn)楸荒銉?yōu)化的代碼有許多難得被執(zhí)行起來(lái)。你花時(shí)間做優(yōu)化是為了讓程序運(yùn)行更快,但如果因?yàn)槿狈?duì)程序的清楚認(rèn)識(shí)而花費(fèi)時(shí)間,那些時(shí)間都是被浪費(fèi)掉了。
第三種性能提升法系利用上述的 "90%" 統(tǒng)計(jì)數(shù)據(jù)。采用這種方法時(shí),你以一種「良好的分解方式」(well-factored manner)來(lái)建造自己的程序,不對(duì)性能投以任何關(guān)切,直至進(jìn)入性能優(yōu)化階段 — 那通常是在開(kāi)發(fā)后期。一旦進(jìn)入該階段,你再按照某個(gè)特定程序來(lái)調(diào)整程序性能。
在性能優(yōu)化階段中,你首先應(yīng)該以一個(gè)量測(cè)工具監(jiān)控程序的運(yùn)行,讓它告訴你程序中哪些地方大量消耗時(shí)間和空間。這樣你就可以找出性能熱點(diǎn)(hot spot)所在的一小段代碼。然后你應(yīng)該集中關(guān)切這些性能熱點(diǎn),并使用前述「持續(xù)關(guān)切法」中的優(yōu)化手段來(lái)優(yōu)化它們。由于你把注意力都集中在熱點(diǎn)上,較少的工作量便可顯現(xiàn)較好的成果。即便如此你還是必須保持謹(jǐn)慎。和重構(gòu)一樣,你應(yīng)該小幅度進(jìn)行修改。每走一步都需要編譯、測(cè)試、再次量測(cè)。如果沒(méi)能提高性能,就應(yīng)該撤銷(xiāo)此次修改。你應(yīng)該繼續(xù)這個(gè)「發(fā)現(xiàn)熱點(diǎn)、去除熱點(diǎn)」的過(guò)程,直到獲得客戶滿意的性能為止。關(guān)于這項(xiàng)技術(shù),McConnell 【McConnell】 為我們提供了更多信息。
一個(gè)被良好分解(well-factored)的程序可從兩方面幫助此種優(yōu)化形式。首先,它讓你有比較充裕的時(shí)間進(jìn)行性能調(diào)整(performance tuning),因?yàn)橛蟹纸饬己玫拇a在手,你就能夠更快速地添加功能,也就有更多時(shí)間用在性能問(wèn)題上(準(zhǔn)確的量測(cè)則保證你把這些時(shí)間投資在恰當(dāng)?shù)攸c(diǎn))。其次,面對(duì)分解良好的程序,你在進(jìn)行性能分析時(shí)便有較細(xì)的粒度(granularity),于是量測(cè)工具把你帶入范圍較小的程序段落中,而性能的調(diào)整也比較容易些。由于代碼更加清晰,因此你能夠更好地理解自己的選擇,更清楚哪種調(diào)整起關(guān)鍵作用。
我發(fā)現(xiàn)重構(gòu)可以幫助我寫(xiě)出更快的軟件。短程看來(lái),重構(gòu)的確會(huì)使軟件變慢,但它使優(yōu)化階段中的軟件性能調(diào)整更容易。最終我還是有賺頭。
優(yōu)化一個(gè)薪資系統(tǒng)— Rich Garzaniti
將Chrysler Comprehensive Compensation(克萊斯勒綜合薪資系統(tǒng))交給GemStone公司之前,我們用了相當(dāng)長(zhǎng)的時(shí)間開(kāi)發(fā)它。開(kāi)發(fā)過(guò)程中我們無(wú)可避免地發(fā)現(xiàn)程序不夠快,于是找了Jim Haungs — GemSmith中的一位好手 — 請(qǐng)他幫我們優(yōu)化這個(gè)系統(tǒng)。
Jim先用一點(diǎn)時(shí)間讓他的團(tuán)隊(duì)了解系統(tǒng)運(yùn)作方式,然后以GemStone的ProfMonitor特性編寫(xiě)出一個(gè)性能量測(cè)工具,將它插入我們的功能測(cè)試中。這個(gè)工具可以顯示系統(tǒng)產(chǎn)生的對(duì)象數(shù)量,以及這些對(duì)象的誕生點(diǎn)。
令我們吃驚的是:創(chuàng)建量最大的對(duì)象竟是字符串。其中最大的工作量則是反復(fù)產(chǎn)生12,000-bytes的字符串。這很特別,因?yàn)檫@字符串實(shí)在太大了,連GemStone慣用的垃圾回收設(shè)施都無(wú)法處理它。由于它是如此巨大,每當(dāng)被創(chuàng)建出來(lái),GemStone都會(huì)將它分頁(yè)(paging)至磁盤(pán)上。也就是說(shuō)字符串的創(chuàng)建竟然用上了I/O子系統(tǒng)(譯注:分頁(yè)機(jī)制會(huì)動(dòng)用I/O),而每次輸出記錄時(shí)都要產(chǎn)生這樣的字符串三次﹗
我們的第一個(gè)解決辦法是把一個(gè)12,000-bytes字符串緩存(cached)起來(lái),這可解決一大半問(wèn)題。后來(lái)我們又加以修改,將它直接寫(xiě)入一個(gè)file stream,從而避免產(chǎn)生字符串。
解決了「巨大字符串」問(wèn)題后,Jim的量測(cè)工具又發(fā)現(xiàn)了一些類(lèi)似問(wèn)題,只不過(guò)字符串稍微小一些:800-bytes、500-bytes……等等,我們也都對(duì)它們改用file stream,于是問(wèn)題都解決了。
使用這些技術(shù),我們穩(wěn)步提高了系統(tǒng)性能。開(kāi)發(fā)過(guò)程中原本似乎需要1,000小時(shí)以上才能完成的薪資計(jì)算,實(shí)際運(yùn)作時(shí)只花40小時(shí)。一個(gè)月后我們把時(shí)間縮短到18小時(shí)。正式投入運(yùn)轉(zhuǎn)時(shí)只花12小時(shí)。經(jīng)過(guò)一年的運(yùn)行和改善后,全部計(jì)算只需9小時(shí)。
我們的最大改進(jìn)就是:將程序放在多處理器(multi-processor)計(jì)算器上,以多線程(multiple threads)方式運(yùn)行。最初這個(gè)系統(tǒng)并非按照多線程思維來(lái)設(shè)計(jì),但由于代碼有良好分解(well factored),所以我們只花三天時(shí)間就讓它得以同時(shí)運(yùn)行多個(gè)線程了。現(xiàn)在,薪資的計(jì)算只需2小時(shí)。
在Jim提供工具使我們得以在實(shí)際操作中量度系統(tǒng)性能之前,我們也猜測(cè)過(guò)問(wèn)題所在。但如果只靠猜測(cè),我們需要很長(zhǎng)的時(shí)間才能試出真正的解法。真實(shí)的量測(cè)指出了一個(gè)完全不同的方向,并大大加快了我們的進(jìn)度。
it知識(shí)庫(kù):代碼重構(gòu),轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。