重構(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提供工具使咱們得以在實際操做中量度系統性能以前,咱們也猜想過問題所在。但若是隻靠猜想,咱們須要很長的時間才能試出真正的解法。真實的量測指出了一個徹底不一樣的方向,並大大加快了咱們的進度。