認識重構

      網友投稿 679 2025-03-31

      1 何謂重構

      我總是不太喜歡下定義,因為每個人對每樣東西都有自己的定義。但是既然在寫書,總得選擇自己滿意的定義。在重構這個概念上,我的定義以Ralph Johnson團隊和其他相關研究成果為基礎。

      首先要說明的是:視上下文不同,“重構”這個詞有兩種不同的定義。你可能會覺得這挺煩人的(我就是這么想的),不過處理自然語言本來就是件煩人的事,這只不過是又一個實例而已。

      第一個定義是名詞形式。

      重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。

      “重構”的另一個用法是動詞形式。

      重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。

      所以,在軟件開發過程中,你可能會花上數小時進行重構,其間可能用上數十種重構手法。

      曾經有人這樣問我:“重構就只是整理代碼嗎?”從某種角度來說,是的。但我認為重構不止于此,因為它提供了一種更高效且受控的代碼整理技術。自從運用重構技術后,我發現自己對代碼的整理比以前更有效率。這是因為我知道該使用哪些重構手法,也知道以怎樣的方式使用它們才能夠將錯誤減到最少,而且在每一個可能出錯的地方我都加以測試。

      我的定義還需要往兩方面擴展。首先,重構的目的是使軟件更容易被理解和修改。你可以在軟件內部做很多修改,但必須對軟件可觀察的外部行為只造成很小變化,或甚至不造成變化。與之形成對比的是性能優化。和重構一樣,性能優化通常不會改變組件的行為(除了執行速度),只會改變其內部結構。但是兩者出發點不同:性能優化往往使代碼較難理解,但為了得到所需的性能你不得不那么做。

      我要強調的第二點是:重構不會改變軟件可觀察的行為——重構之后軟件功能一如以往。任何用戶,不論最終用戶或其他程序員,都不知道已經有東西發生了變化。

      上述第二點引出了Kent Beck的“兩頂帽子”比喻。使用重構技術開發軟件時,你把自己的時間分配給兩種截然不同的行為:添加新功能,以及重構。添加新功能時,你不應該修改既有代碼,只管添加新功能。通過測試(并讓測試正常運行),你可以衡量自己的工作進度。重構時你就不能再添加功能,只管改進程序結構。此時你不應該添加任何測試(除非發現有先前遺漏的東西),只在絕對必要(用以處理接口變化)時才修改測試。

      軟件開發過程中,你可能會發現自己經常變換帽子。首先你會嘗試添加新功能,然后會意識到:如果把程序結構改一下,功能的添加會容易得多。于是你換一頂帽子,做一會兒重構工作。程序結構調整好后,你又換上原先的帽子,繼續添加新功能。新功能正常工作后,你又發現自己的編碼造成程序難以理解,于是又換上重構帽子……整個過程或許只花十分鐘,但無論何時你都應該清楚自己戴的是哪一頂帽子。

      2 為何重構

      我不想把重構說成是包治百病的萬靈丹,它絕對不是所謂的“銀彈”。不過它的確很有價值,雖不是一顆銀***卻是一把“銀鉗子”,可以幫助你始終良好地控制自己的代碼。重構是個工具,它可以(并且應該)用于以下幾個目的。

      如果沒有重構,程序的設計會逐漸腐敗變質。當人們只為短期目的,或是在完全理解整體設計之前,就貿然修改代碼,程序將逐漸失去自己的結構,程序員越來越難通過閱讀源碼而理解原來的設計。重構很像是在整理代碼,你所做的就是讓所有東西回到應處的位置上。代碼結構的流失是累積性的。越難看出代碼所代表的設計意圖,就越難保護其中設計,于是該設計就腐敗得越快。經常性的重構可以幫助代碼維持自己該有的形態。

      完成同樣一件事,設計不良的程序往往需要更多代碼,這常常是因為代碼在不同的地方使用完全相同的語句做同樣的事。因此改進設計的一個重要方向就是消除重復代碼。這個動作的重要性在于方便未來的修改。代碼量減少并不會使系統運行更快,因為這對程序的運行軌跡幾乎沒有任何明顯影響。然而代碼量減少將使未來可能的程序修改動作容易得多。代碼越多,正確的修改就越困難,因為有更多代碼需要理解。你在這兒做了點修改,系統卻不如預期那樣工作,是因為你沒有修改另一處——那兒的代碼做著幾乎完全一樣的事情,只是所處環境略有不同。如果消除重復代碼,你就可以確定所有事物和行為在代碼中只表述一次,這正是優秀設計的根本。

      所謂程序設計,很大程度上就是與計算機交談:你編寫代碼告訴計算機做什么事,它的響應則是精確按照你的指示行動。你得及時填補“想要它做什么”和“告訴它做什么”之間的縫隙。這種編程模式的核心就是“準確說出我所要的”。除了計算機外,你的源碼還有其他讀者:幾個月之后可能會有另一位程序員嘗試讀懂你的代碼并做一些修改。我們很容易忘記這第二位讀者,但他才是最重要的。計算機是否多花了幾個小時來編譯,又有什么關系呢?如果一個程序員花費一周時間來修改某段代碼,那才要命呢——如果他理解了你的代碼,這個修改原本只需一小時。

      問題在于,當你努力讓程序運轉的時候,不會想到未來出現的那個開發者。是的,我們應該改變一下開發節奏,對代碼做適當修改,讓代碼變得更易理解。重構可以幫助我們讓代碼更易讀。一開始進行重構時,你的代碼可以正常運行,但結構不夠理想。在重構上花一點點時間,就可以讓代碼更好地表達自己的用途。這種編程模式的核心就是“準確說出我所要的”。

      關于這一點,我沒必要表現得如此無私。很多時候那個未來的開發者就是我自己。此時重構就顯得尤其重要了。我是個很懶惰的程序員,我的懶惰表現形式之一就是:總是記不住自己寫過的代碼。事實上,對于任何能夠立刻查閱的東西,我都故意不去記它,因為我怕把自己的腦袋塞爆。我總是盡量把該記住的東西寫進程序里,這樣我就不必記住它了。這么一來我就不必太擔心Old Peculier[1][Jackson]殺光我的腦細胞。

      這種可理解性還有另一方面的作用。我利用重構來協助我理解不熟悉的代碼。每當看到不熟悉的代碼,我必須試著理解其用途。我先看兩行代碼,然后對自己說:“噢,是的,它做了這些那些……”有了重構這個強大武器在手,我不會滿足于這么一點體會。我會真正動手修改代碼,讓它更好地反映出我的理解,然后重新執行,看它是否仍然正常運作,以此檢驗我的理解是否正確。

      一開始我所做的重構都像這樣停留在細枝末節上。隨著代碼漸趨簡潔,我發現自己可以看到一些以前看不到的設計層面的東西。如果不對代碼做這些修改,也許我永遠看不見它們,因為我的聰明才智不足以在腦子里把這一切都想象出來。Ralph Johnson把這種“早期重構”描述為“擦掉窗戶上的污垢,使你看得更遠”。研究代碼時我發現,重構把我帶到更高的理解層次上。如果沒有重構,我達不到這種層次。

      對代碼的理解,可以幫助我找到bug。我承認我不太擅長調試。有些人只要盯著一大段代碼就可以找出里面的bug,我可不行。但我發現,如果對代碼進行重構,我就可以深入理解代碼的作為,并恰到好處地把新的理解反饋回去。搞清楚程序結構的同時,我也清楚了自己所做的一些假設,于是想不把bug揪出來都難。

      這讓我想起了Kent Beck經常形容自己的一句話:“我不是個偉大的程序員,我只是個有著一些優秀習慣的好程序員。”重構能夠幫助我更有效地寫出強健的代碼。

      終于,前面的一切都歸結到了這最后一點:重構幫助你更快速地開發程序。

      聽起來有點違反直覺。當我談到重構,人們很容易看出它能夠提高質量。改善設計、提升可讀性、減少錯誤,這些都是提高質量。但這難道不會降低開發速度嗎?

      我絕對相信:良好的設計是快速開發的根本——事實上,擁有良好設計才可能做到快速開發。如果沒有良好設計,或許某一段時間內你的進展迅速,但惡劣的設計很快就讓你的速度慢下來。你會把時間花在調試上面,無法添加新功能。修改時間越來越長,因為你必須花越來越多的時間去理解系統、尋找重復代碼。隨著你給最初程序打上一個又一個的補丁,新特性需要更多代碼才能實現。真是個惡性循環。

      良好設計是維持軟件開發速度的根本。重構可以幫助你更快速地開發軟件,因為它阻止系統腐敗變質,它甚至還可以提高設計質量。

      3 何時重構

      當我談論重構,常常有人問我應該怎樣安排重構時間表。我們是不是應該每兩個月就專門安排兩個星期來進行重構呢?

      幾乎任何情況下我都反對專門撥出時間進行重構。在我看來,重構本來就不是一件應該特別撥出時間做的事情,重構應該隨時隨地進行。你不應該為重構而重構,你之所以重構,是因為你想做別的什么事,而重構可以幫助你把那些事做好。

      Don Roberts給了我一條準則:第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構。

      事不過三,三則重構。

      最常見的重構時機就是我想給軟件添加新特性的時候。此時,重構的直接原因往往是為了幫助我理解需要修改的代碼——這些代碼可能是別人寫的,也可能是我自己寫的。無論何時,只要我想理解代碼所做的事,我就會問自己:是否能對這段代碼進行重構,使我能更快地理解它。然后我就會重構。之所以這么做,部分原因是為了讓我下次再看這段代碼時容易理解,但最主要的原因是:如果在前進過程中把代碼結構理清,我就可以從中理解更多東西。

      在這里,重構的另一個原動力是:代碼的設計無法幫助我輕松添加我所需要的特性。我看著設計,然后對自己說:“如果用某種方式來設計,添加特性會簡單得多。”這種情況下我不會因為自己過去的錯誤而懊惱——我用重構來彌補它。之所以這么做,部分原因是為了讓未來增加新特性時能夠更輕松一些,但最主要的原因還是:我發現這是最快捷的途徑。重構是一個快速流暢的過程,一旦完成重構,新特性的添加就會更快速、更流暢。

      調試過程中運用重構,多半是為了讓代碼更具可讀性。當我看著代碼并努力理解它的時候,我用重構幫助加深自己的理解。我發現以這種程序來處理代碼,常常能夠幫助我找出bug。你可以這么想:如果收到一份錯誤報告,這就是需要重構的信號,因為顯然代碼還不夠清晰——沒有清晰到讓你能一眼看出bug。

      很多公司都會做常規的代碼復審,因為這種活動可以改善開發狀況。這種活動有助于在開發團隊中傳播知識,也有助于讓較有經驗的開發者把知識傳遞給比較欠缺經驗的人,并幫助更多人理解大型軟件系統中的更多部分。代碼復審對于編寫清晰代碼也很重要。我的代碼也許對我自己來說很清晰,對他人則不然。這是無法避免的,因為要讓開發者設身處地為那些不熟悉自己所做所為的人著想,實在太困難了。代碼復審也讓更多人有機會提出有用的建議,畢竟我在一個星期之內能夠想出的好點子很有限。如果能得到別人的幫助,我的生活會滋潤得多,所以我總是期待更多復審。

      我發現,重構可以幫助我復審別人的代碼。開始重構前我可以先閱讀代碼,得到一定程度的理解,并提出一些建議。一旦想到一些點子,我就會考慮是否可以通過重構立即輕松地實現它們。如果可以,我就會動手。這樣做了幾次以后,我可以把代碼看得更清楚,提出更多恰當的建議。我不必想象代碼應該是什么樣,我可以“看見”它是什么樣。于是我可以獲得更高層次的認識。如果不進行重構,我永遠無法得到這樣的認識。

      重構還可以幫助代碼復審工作得到更具體的結果。不僅獲得建議,而且其中許多建議能夠立刻實現。最終你將從實踐中得到比以往多得多的成就感。

      如果是比較大的設計復審工作,那么在一個較大團隊內保留多種觀點通常會更好一些。此時直接展示代碼往往不是最佳辦法。我喜歡運用UML示意圖展現設計,并以CRC卡展示軟件情節。換句話說,我會和某個團隊進行設計復審,而和單個復審者進行代碼復審。

      極限編程[Beck,XP]中的“結對編程”形式,把代碼復審的積極性發揮到了極致。一旦采用這種形式,所有正式開發任務都由兩名開發者在同一臺機器上進行。這樣便在開發過程中形成隨時進行的代碼復審工作,而重構也就被包含在開發過程內了。

      為什么重構有用

      ——Kent Beck

      程序有兩面價值:“今天可以為你做什么”和“明天可以為你做什么”。大多數時候,我們都只關注自己今天想要程序做什么。不論是修復錯誤或是添加特性,我們都是為了讓程序能力更強,讓它在今天更有價值。

      但是系統當下的行為,只是整個故事的一部分,如果沒有認清這一點,你無法長期從事編程工作。如果你為求完成今天的任務而不擇手段,導致不可能在明天完成明天的任務,那么最終還是會失敗。但是,你知道自己今天需要什么,卻不一定知道自己明天需要什么。也許你可以猜到明天的需求,也許吧,但肯定還有些事情出乎你的意料。

      對于今天的工作,我了解得很充分;對于明天的工作,我了解得不夠充分。但如果我純粹只是為今天工作,明天我將完全無法工作。

      重構是一條擺脫困境的道路。如果你發現昨天的決定已經不適合今天的情況,放心改變這個決定就是,然后你就可以完成今天的工作了。明天,喔,明天回頭看今天的理解也許覺得很幼稚,那時你還可以改變你的理解。

      是什么讓程序如此難以相與? 眼下我能想起下述四個原因,它們是:

      難以閱讀的程序,難以修改;

      邏輯重復的程序,難以修改;

      添加新行為時需要修改已有代碼的程序,難以修改;

      帶復雜條件邏輯的程序,難以修改。

      因此,我們希望程序:(1) 容易閱讀;(2) 所有邏輯都只在唯一地點指定;(3) 新的改動不會危及現有行為;(4) 盡可能簡單表達條件邏輯。

      重構是這樣一個過程:它在一個目前可運行的程序上進行,在不改變程序行為的前提下使其具備上述美好性質,使我們能夠繼續保持高速開發,從而增加程序的價值。

      4 怎么對經理說

      “該怎么跟經理說重構的事?”這是我最常被問到的一個問題。如果這位經理懂技術,那么向他介紹重構應該不會很困難。如果這位經理只對質量感興趣,那么問題就集中到了“質量”上面。此時,在復審過程中使用重構就是一個不錯的辦法。大量研究結果顯示,技術復審是減少錯誤、提高開發速度的一條重要途徑。隨便找一本關于復審、審查或軟件開發程序的書看看,從中找些最新引證,應該可以讓大多數經理認識復審的價值。然后你就可以把重構當作“將復審意見引入代碼內”的方法來使用,這很容易。

      當然,很多經理嘴巴上說自己“質量驅動”,其實更多是“進度驅動”。這種情況下我會給他們一個較有爭議的建議:不要告訴經理!

      這是在搞破壞嗎?我不這樣想。軟件開發者都是專業人士。我們的工作就是盡可能快速創造出高效軟件。我的經驗告訴我,對于快速創造軟件,重構可帶來巨大幫助。如果需要添加新功能,而原本設計卻又使我無法方便地修改,我發現先重構再添加新功能會更快些。如果要修補錯誤,就得先理解軟件的工作方式,而我發現重構是理解軟件的最快方式。受進度驅動的經理要我盡可能快速完事,至于怎么完成,那就是我的事了。我認為最快的方式就是重構,所以我就重構。

      間接層和重構

      ——Kent Beck

      “計算機科學是這樣一門科學:它相信所有問題都可以通過增加一個間接層來解決。”

      ——Dennis DeBruler

      由于軟件工程師對間接層如此醉心,你應該不會驚訝大多數重構都為程序引入了更多間接層。重構往往把大型對象拆成多個小型對象,把大型函數拆成多個小型函數。

      但是,間接層是一柄雙刃劍。每次把一個東西分成兩份,你就需要多管理一個東西。如果某個對象委托另一對象,后者又委托另一對象,程序會愈加難以閱讀。

      基于這個觀點,你會希望盡量減少間接層。

      別急,伙計!間接層有它的價值。下面就是間接層的某些價值。

      允許邏輯共享。比如說一個子函數在兩個不同的地點被調用,或超類中的某個函數被所有子類共享。

      分開解釋意圖和實現。你可以選擇每個類和函數的名字,這給了你一個解釋自己意圖的機會。類或函數內部則解釋實現這個意圖的做法。如果類和函數內部又以更小單元的意圖來編寫,你所寫的代碼就可以描述其結構中的大部分重要信息。

      隔離變化。很可能我在兩個不同地點使用同一對象,其中一個地點我想改變對象行為,但如果修改了它,我就要冒同時影響兩處的風險。為此我做出一個子類,并在需要修改處引用這個子類。現在,我可以修改這個子類而不必承擔無意中影響另一處的風險。

      封裝條件邏輯。對象有一種奇妙的機制:多態消息,可以靈活而清晰地表達條件邏輯。將條件邏輯轉化為消息形式,往往能降低代碼的重復、增加清晰度并提高彈性。

      這就是重構游戲:在保持系統現有行為的前提下,如何才能提高系統的質量或降低其成本,從而使它更有價值?

      這個游戲中最常見的變量就是:你如何看待你自己的程序。找出一個缺乏“間接層利益”之處,在不修改現有行為的前提下,為它加入一個間接層。現在你獲得了一個更有價值的程序,因為它有較高的質量,讓我們在明天(未來)受益。

      請將這種方法與“小心翼翼的事前設計”做個比較。推測性設計總是試圖在任何一行代碼誕生之前就先讓系統擁有所有優秀質量,然后程序員將代碼塞進這個強健的骨架中就行了。這個過程的問題在于:太容易猜錯。如果運用重構,你就永遠不會面臨全盤錯誤的危險。程序自始至終都能保持一致的行為,而你又有機會為程序添加更多價值不菲的質量。

      還有一種比較少見的重構游戲:找出不值得的間接層,并將它拿掉。這種間接層常以中介函數形式出現,它也許曾經有過貢獻,但芳華已逝。它也可能是個組件,你本來期望在不同地點共享它,或讓它表現出多態性,最終卻只在一處用到。如果你找到這種“寄生式間接層”,請把它扔掉。如此一來你會獲得一個更有價值的程序,不是因為它取得了更多的優秀質量,而是因為它以更少的間接層獲得一樣多的優秀質量。

      5 重構的難題

      學習一種可以大幅提高生產力的新技術時,你總是難以察覺其不適用的場合。通常你在一個特定場景中學習它,這個場景往往是個項目。這種情況下你很難看出什么會造成這種新技術成效不彰甚或形成危害。十年前,對象技術的情況也是如此。那時如果有人問我何時不要使用對象,我很難回答。并非我認為對象十全十美、沒有局限性——我最反對這種盲目態度,而是盡管我知道它的好處,但確實不知道其局限性在哪兒。

      現在,重構的處境也是如此。我們知道重構的好處,我們知道重構可以給我們的工作帶來立竿見影的改變。但是我們還沒有獲得足夠的經驗,我們還看不到它的局限性。

      認識重構

      隨著更多人學會重構技巧,我們也將對它有更多了解。對你而言這意味著:雖然我堅決認為你應該嘗試一下重構,獲得它所提供的利益,但與此同時,你也應該時時監控其過程,注意尋找重構可能引入的問題。請讓我們知道你所遭遇的問題。隨著對重構的了解日益增多,我們將找出更多解決辦法,并清楚知道哪些問題是真正難以解決的。

      重構經常出問題的一個領域就是數據庫。絕大多數商用程序都與它們背后的數據庫結構緊密耦合在一起,這也是數據庫結構如此難以修改的原因之一。另一個原因是數據遷移(migration)。就算你非常小心地將系統分層,將數據庫結構和對象模型間的依賴降至最低,但數據庫結構的改變還是讓你不得不遷移所有數據,這可能是件漫長而煩瑣的工作。

      在非對象數據庫中,解決這個問題的辦法之一就是:在對象模型和數據庫模型之間插入一個分隔層,這就可以隔離兩個模型各自的變化。升級某一模型時無需同時升級另一模型,只需升級上述的分隔層即可。這樣的分隔層會增加系統復雜度,但可以給你帶來很大的靈活度。如果你同時擁有多個數據庫,或如果數據庫模型較為復雜使你難以控制,那么即使不進行重構,這分隔層也是很重要的。

      你無需一開始就插入分隔層,可以在發現對象模型變得不穩定時再產生它,這樣你就可以為你的改變找到最好的平衡點。

      對開發者而言,對象數據庫既有幫助也有妨礙。某些面向對象數據庫提供不同版本的對象之間的自動遷移功能,這減少了數據遷移時的工作量,但還是會損失一定時間。如果各數據庫之間的數據遷移并非自動進行,你就必須自行完成遷移工作,這個工作量可是很大的。這種情況下你必須更加留神類中的數據結構變化。你仍然可以放心將類的行為轉移過去,但轉移字段時就必須格外小心。數據尚未被轉移前你就得先運用訪問函數造成“數據已經轉移”的假象。一旦你確定知道數據應該放在何處,就可以一次性地將數據遷移過去。這時唯一需要修改的只有訪問函數,這也降低了錯誤風險[2]。

      關于對象,另一件重要事情是:它們允許你分開修改軟件模塊的實現和接口。你可以安全地修改對象內部實現而不影響他人,但對于接口要特別謹慎——如果接口被修改了,任何事情都有可能發生。

      一直對重構帶來困擾的一件事就是:許多重構手法的確會修改接口。像Rename Method (273)這么簡單的重構手法所做的一切就是修改接口。這對極為珍貴的封裝概念會帶來什么影響呢?

      如果某個函數的所有調用者都在你的控制之下,那么即使修改函數名稱也不會有任何問題。哪怕面對一個public函數,只要能取得并修改其所有調用者,你也可以安心地將這個函數改名。只有當需要修改的接口被那些“找不到,即使找到也不能修改”的代碼使用時,接口的修改才會成為問題。如果情況真是如此,我就會說:這個接口是個已發布接口(published interface)——比公開接口(public interface)更進一步。接口一旦發布,你就再也無法僅僅修改調用者而能夠安全地修改接口了。你需要一個更復雜的流程。

      這個想法改變了我們的問題。如今的問題是:該如何面對那些必須修改“已發布接口”的重構手法?

      簡言之,如果重構手法改變了已發布接口,你必須同時維護新舊兩個接口,直到所有用戶都有時間對這個變化做出反應。幸運的是,這不太困難。你通常都有辦法把事情組織好,讓舊接口繼續工作。請盡量這么做:讓舊接口調用新接口。當你要修改某個函數名稱時,請留下舊函數,讓它調用新函數。千萬不要復制函數實現,那會讓你陷入重復代碼的泥淖中難以自拔。你還應該使用Java提供的deprecation(不建議使用)設施,將舊接口標記為deprecated。這么一來你的調用者就會注意到它了。

      這個過程的一個好例子就是Java容器類(集合類,collection classes)。Java 2的新容器取代了原先一些容器。當Java 2容器發布時,JavaSoft花了很大力氣來為開發者提供一條順利遷徙之路。

      “保留舊接口”的辦法通常可行,但很煩人。起碼在一段時間里你必須構造并維護一些額外的函數。它們會使接口變得復雜,使接口難以使用。還好我們有另一個選擇:不要發布接口。當然我不是說要完全禁止,因為很明顯你總得發布一些接口。如果你正在建造供外部使用的API(就像Sun公司所做的那樣),就必須發布接口。之所以說盡量不要發布,是因為我常常看到一些開發團隊公開了太多接口。我曾經看到一支三人團隊這么工作:每個人都向另外兩人公開發布接口。這使他們不得不經常來回維護接口,而其實他們原本可以直接進入程序庫,徑行修改自己管理的那一部分,那會輕松許多。過度強調代碼所有權的團隊常常會犯這種錯誤。發布接口很有用,但也有代價。所以除非真有必要,不要發布接口。這可能意味需要改變你的代碼所有權觀念,讓每個人都可以修改別人的代碼,以適應接口的改動。以結對編程的方式完成這一切通常是個好主意。

      不要過早發布接口。請修改你的代碼所有權政策,使重構更順暢。

      Java還有一種特別的接口修改:在throws子句中增加一個異常。這并不是對函數簽名的修改,所以你無法以委托的辦法隱藏它;但如果用戶代碼不做出相應修改,編譯器不會讓它通過。這個問題很難解決。你可以為這個函數選擇一個新名字,讓舊函數調用它,并將這個新增的受控異常轉換成一個非受控異常。你也可以拋出一個非受控異常,不過這樣你就會失去檢驗能力。如果你那么做,你可以警告調用者:這個非受控異常日后會變成一個受控異常。這樣他們就有時間在自己的代碼中加上對此異常的處理。出于這個原因,我總是喜歡為整個包(package)定義一個異常基類(就像java.sql的SQLException),并確保所有public函數只在自己的throws子句中聲明這個異常。這樣我就可以隨心所欲地定義異常子類,不會影響調用者,因為調用者永遠只知道那個更具一般性的異常基類。

      通過重構,可以排除所有設計錯誤嗎?是否存在某些核心設計決策,無法以重構手法修改?在這個領域里,我們的統計數據尚不完整。當然某些情況下我們可以很有效地重構,這常常令我們倍感驚訝,但的確也有難以重構的地方。比如說在一個項目中,我們很難(但還是有可能)將不考慮安全性需求時構造起來的系統重構為具備良好安全性系統。

      這種情況下我的辦法就是:先想象重構的情況。考慮候選設計方案時,我會問自己:將某個設計重構為另一個設計的難度有多大?如果看上去很簡單,我就不必太擔心選擇是否得當,于是我就會選最簡單的設計,哪怕它不能覆蓋所有潛在需求也沒關系。但如果預先看不到簡單的重構辦法,我就會在設計上投入更多力氣。不過我發現,后一種情況很少出現。

      有時候你根本不應該重構,例如當你應該重新編寫所有代碼的時候。有時候既有代碼實在太混亂,重構它還不如重新寫一個來得簡單。作出這種決定很困難,我承認我也沒有什么好準則可以判斷何時應該放棄重構。

      重寫(而非重構)的一個清楚訊號就是:現有代碼根本不能正常運作。你可能只是試著做點測試,然后就發現代碼中滿是錯誤,根本無法穩定運作。記住,重構之前,代碼必須起碼能夠在大部分情況下正常運作。

      一個折中辦法就是:將“大塊頭軟件”重構為封裝良好的小型組件。然后你就可以逐一對組件做出“重構或重建”的決定。這是一個頗有希望的辦法,但我還沒有足夠數據,所以也無法寫出好的指導原則。對于一個重要的遺留系統,這肯定會是一個很好的方向。

      另外,如果項目已近最后期限,你也應該避免重構。在此時機,從重構過程贏得的生產力只有在最后期限過后才能體現出來,而那個時候已經為時晚矣。Ward Cunningham對此有一個很好的看法。他把未完成的重構工作形容為“債務”。很多公司都需要借債來使自己更有效地運轉。但是借債就得付利息,過于復雜的代碼所造成的維護和擴展的額外成本就是利息。你可以承受一定程度的利息,但如果利息太高你就會被壓垮。把債務管理好是很重要的,你應該隨時通過重構來償還一部分債務。

      如果項目已經非常接近最后期限,你不應該再分心于重構,因為已經沒有時間了。不過多個項目經驗顯示:重構的確能夠提高生產力。如果最后你沒有足夠時間,通常就表示你其實早該進行重構。

      6 重構與設計

      重構肩負一項特殊使命:它和設計彼此互補。初學編程的時候,我埋頭就寫程序,渾渾噩噩地進行開發。然而很快我便發現,事先做好設計可以讓我節省返工的高昂成本。于是我很快加強這種“預先設計”風格。許多人都把設計看做軟件開發的關鍵環節,而把編程看做只是機械式的低級勞動。他們認為設計就像畫工程圖而編碼就像施工。但是你要知道,軟件和機器有著很大的差異:軟件的可塑性更強,而且完全是思想產品。正如Alistair Cockburn所說:“有了設計,我可以思考得更快,但是其中充滿小漏洞。”

      有一種觀點認為:重構可以取代預先設計。這意思是你根本不必做任何設計,只管按照最初想法開始編碼,讓代碼有效運作,然后再將它重構成型。事實上這種辦法真的可行。我的確看過有人這么做,最后獲得設計良好的軟件。極限編程[Beck,XP]的支持者極力提倡這種辦法。

      盡管如上所言,只運用重構也能收到效果,但這并不是最有效的途徑。是的,就連極限編程的愛好者們也會進行預先設計。他們會使用CRC卡或類似的東西來檢驗各種不同想法,然后才得到第一個可被接受的解決方案,然后才能開始編碼,然后才能重構。關鍵在于:重構改變了預先設計的角色。如果沒有重構,你就必須保證預先做出的設計正確無誤,這個壓力太大了。這意味如果將來需要對原始設計做任何修改,代價都將非常高昂。因此你需要把更多時間和精力放在預先設計上,以避免日后修改。

      如果你選擇重構,問題的重點就轉變了。你仍然做預先設計,但是不必一定找出正確的解決方案。此刻的你只需要得到一個足夠合理的解決方案就夠了。你很肯定地知道,在實現這個初始解決方案的時候,你對問題的理解也會逐漸加深,你可能會察覺最佳解決方案和你當初設想的有些不同。只要有重構這把利器在手,就不成問題,因為重構讓日后的修改成本不再高昂。

      這種轉變導致一個重要結果:軟件設計向簡化前進了一大步。過去未曾運用重構時,我總是力求得到靈活的解決方案。任何一個需求都讓我提心吊膽地猜疑:在系統的有生之年,這個需求會導致怎樣的變化?由于變更設計的代價非常高昂,所以我希望建造一個足夠靈活、足夠牢靠的解決方案,希望它能承受我所能預見的所有需求變化。問題在于:要建造一個靈活的解決方案,所需的成本難以估算。靈活的解決方案比簡單的解決方案復雜許多,所以最終得到的軟件通常也會更難維護——雖然它在我預先設想的方向上的確是更加靈活。就算幸運地走在預先設想的方向上,你也必須理解如何修改設計。如果變化只出現在一兩個地方,那不算大問題。然而變化其實可能出現在系統各處。如果在所有可能的變化出現地點都建立起靈活性,整個系統的復雜度和維護難度都會大大提高。當然,如果最后發現所有這些靈活性都毫無必要,這才是最大的失敗。你知道,這其中肯定有些靈活性的確派不上用場,但你卻無法預測到底是哪些派不上用場。為了獲得自己想要的靈活性,你不得不加入比實際需要更多的靈活性。

      有了重構,你就可以通過一條不同的途徑來應付變化帶來的風險。你仍舊需要思考潛在的變化,仍舊需要考慮靈活的解決方案。但是你不必再逐一實現這些解決方案,而是應該問問自己:“把一個簡單的解決方案重構成這個靈活的方案有多大難度?”如果答案是“相當容易”(大多數時候都如此),那么你就只需實現目前的簡單方案就行了。

      重構可以帶來更簡單的設計,同時又不損失靈活性,這也降低了設計過程的難度,減輕了設計壓力。一旦對重構帶來的簡單性有更多感受,你甚至可以不必再預先思考前述所謂的靈活方案——一旦需要它,你總有足夠的信心去重構。是的,當下只管建造可運行的最簡化系統,至于靈活而復雜的設計,唔,多數時候你都不會需要它。

      勞而無獲

      ——Ron Jeffries

      克萊斯勒綜合薪資系統的支付過程太慢了。雖然我們的開發還沒結束,這個問題卻已經開始困擾我們,因為它已經拖累了測試速度。

      Kent Beck、Martin Fowler和我決定解決這個問題。等待大伙兒會合的時間里,憑著我對這個系統的全盤了解,我開始推測:到底是什么讓系統變慢了?我想到數種可能,然后和伙伴們談了幾種可能的修改方案。最后,我們就“如何讓這個系統運行更快”,提出了一些真正的好點子。

      然后,我們拿Kent的工具度量了系統性能。我一開始所想的可能性竟然全都不是問題肇因。我們發現:系統把一半時間用來創建“日期”實例(instance)。更有趣的是,所有這些實例都有相同的值。

      于是我們觀察日期的創建邏輯,發現有機會將它優化。日期原本是由字符串轉換而成,即使無外部輸入也是如此。之所以使用字符串轉換方式,完全是為了方便鍵盤輸入。好,也許我們可以優化它。

      于是我們觀察這個程序如何使用日期對象。我們發現,很多日期對象都被用來產生“日期區間”實例——后者由一個起始日期和一個結束日期組成。仔細追蹤下去,我們發現絕大多數日期區間是空的!

      處理日期區間時我們遵循這樣一個規則:如果結束日期在起始日期之前,這個日期區間就該是空的。這是一條很好的規則,完全符合這個類的需要。采用此一規則后不久,我們意識到,創建一個“起始日期在結束日期之后”的日期區間,仍然不算是清晰的代碼,于是我們把這個行為提煉成一個工廠函數,由它專門創建“空的日期區間”。

      我們做了上述修改,使代碼更加清晰,也意外得到了一個驚喜:可以創建一個固定不變的“空日期區間”對象,并讓上述調整后的工廠函數始終返回該對象,而不再每次都創建新對象。這一修改把系統速度提升了幾乎一倍,足以讓測試速度達到可接受程度。這只花了我們大約五分鐘。

      我和團隊成員(Kent和Martin謝絕參加)認真推測過:我們了若指掌的這個程序中可能有什么錯誤?我們甚至憑空做了些改進設計,卻沒有先對系統的真實情況進行度量。我們完全錯了。除了一場很有趣的交談,我們什么好事都沒做。

      教訓:哪怕你完全了解系統,也請實際度量它的性能,不要臆測。臆測會讓你學到一些東西,但十有八九你是錯的。

      7 重構與性能

      關于重構,有一個常被提出的問題:它對程序的性能將造成怎樣的影響?為了讓軟件易于理解,你常會做出一些使程序運行變慢的修改。這是個重要的問題。我并不贊成為了提高設計的純潔性而忽視性能,把希望寄托于更快的硬件身上也絕非正道。已經有很多軟件因為速度太慢而被用戶拒絕,日益提高的機器速度也只不過略微放寬了速度方面的限制而已。但是,換個角度說,雖然重構可能使軟件運行更慢,但它也使軟件的性能優化更容易。除了對性能有嚴格要求的實時系統,其他任何情況下“編寫快速軟件”的秘密就是:首先寫出可調的軟件,然后調整它以求獲得足夠速度。

      我看過三種編寫快速軟件的方法。其中最嚴格的是時間預算法,這通常只用于性能要求極高的實時系統。如果使用這種方法,分解你的設計時就要做好預算,給每個組件預先分配一定資源——包括時間和執行軌跡。每個組件絕對不能超出自己的預算,就算擁有組件之間調度預配時間的機制也不行。這種方法高度重視性能,對于心律調節器一類的系統是必須的,因為在這樣的系統中遲來的數據就是錯誤的數據。但對其他系統(例如我經常開發的企業信息系統)而言,如此追求高性能就有點過分了。

      第二種方法是持續關注法。這種方法要求任何程序員在任何時間做任何事時,都要設法保持系統的高性能。這種方式很常見,感覺上很有吸引力,但通常不會起太大作用。任何修改如果是為了提高性能,通常會使程序難以維護,繼而減緩開發速度。如果最終得到的軟件的確更快了,那么這點損失尚有所值,可惜通常事與愿違,因為性能改善一旦被分散到程序各角落,每次改善都只不過是從對程序行為的一個狹隘視角出發而已。

      關于性能,一件很有趣的事情是:如果你對大多數程序進行分析,就會發現它把大半時間都耗費在一小半代碼身上。如果你一視同仁地優化所有代碼,90%的優化工作都是白費勁的,因為被你優化的代碼大多很少被執行。你花時間做優化是為了讓程序運行更快,但如果因為缺乏對程序的清楚認識而花費時間,那些時間就都被浪費掉了。

      第三種性能提升法就是利用上述的90%統計數據。采用這種方法時,你編寫構造良好的程序,不對性能投以特別的關注,直至進入性能優化階段——那通常是在開發后期。一旦進入該階段,你再按照某個特定程序來調整程序性能。

      在性能優化階段,你首先應該用一個度量工具來監控程序的運行,讓它告訴你程序中哪些地方大量消耗時間和空間。這樣你就可以找出性能熱點所在的一小段代碼。然后你應該集中關注這些性能熱點,并使用持續關注法中的優化手段來優化它們。由于你把注意力都集中在熱點上,較少的工作量便可顯現較好的成果。即便如此你還是必須保持謹慎。和重構一樣,你應該小幅度進行修改。每走一步都需要編譯、測試、再次度量。如果沒能提高性能,就應該撤銷此次修改。你應該繼續這個“發現熱點、去除熱點”的過程,直到獲得客戶滿意的性能為止。關于這項技術,McConnell[McConnell]為我們提供了更多信息。

      一個構造良好的程序可從兩方面幫助這一優化形式。首先,它讓你有比較充裕的時間進行性能調整,因為有構造良好的代碼在手,你就能夠更快速地添加功能,也就有更多時間用在性能問題上(準確的度量則保證你把這些時間投資在恰當地點)。其次,面對構造良好的程序,你在進行性能分析時便有較細的粒度,于是度量工具把你帶入范圍較小的程序段落中,而性能的調整也比較容易些。由于代碼更加清晰,因此你能夠更好地理解自己的選擇,更清楚哪種調整起關鍵作用。

      我發現重構可以幫助我寫出更快的軟件。短期看來,重構的確可能使軟件變慢,但它使優化階段的軟件性能調整更容易,最終還是會得到好的效果。

      8 重構起源何處

      我曾經努力想找出重構(refactoring)一詞的真正起源,但最終失敗了。優秀程序員肯定至少會花一些時間來清理自己的代碼。這么做是因為,他們知道簡潔的代碼比雜亂無章的代碼更容易修改,而且他們知道自己幾乎無法一開始就寫出簡潔的代碼。

      [1] 一種有名的麥芽酒。——譯者注

      [2] 數據庫重構的經驗也已經由Soctt Ambler等人總結成書,相關內容請參考《數據庫重構》(http://www.douban.com/subject/1954438/)。——譯者注

      本文節選自《重構:改善既有代碼的設計》

      內容簡介

      本書清晰揭示了重構的過程,解釋了重構的原理和最佳實踐方式,并給出了何時以及何地應該開始挖掘代碼以求改善。書中給出了70 多個可行的重構,每個重構都介紹了一種經過驗證的代碼變換手法的動機和技術。本書提出的重構準則將幫助你一次一小步地修改你的代碼,從而減少了開發過程中的風險。

      本書適合軟件開發人員、項目管理人員等閱讀,也可作為高等院校計算機及相關專業師生的參考讀物。

      本文轉載自異步社區

      軟件開發 軟件開發

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel自動填充相同內容(excel 填充相同內容)
      下一篇:如何在excel設置返回目錄
      相關文章
      亚洲Av无码国产情品久久| 亚洲天堂一区二区三区| 中文字幕无码精品亚洲资源网久久 | 精品无码一区二区三区亚洲桃色| 亚洲AV永久无码精品成人| 亚洲午夜国产精品无码| 久久夜色精品国产亚洲av| 中文字幕专区在线亚洲| 亚洲一区二区三区自拍公司| 亚洲裸男gv网站| 区久久AAA片69亚洲| 国产亚洲情侣一区二区无码AV| 亚洲国产午夜中文字幕精品黄网站| 亚洲av成人一区二区三区在线播放| 亚洲国产精品无码第一区二区三区| 亚洲乱妇老熟女爽到高潮的片| 亚洲欧洲无码AV不卡在线| 亚洲国产无线乱码在线观看| 日韩色日韩视频亚洲网站| 亚洲成a人片在线观看国产| 亚洲毛片网址在线观看中文字幕| 久久精品国产亚洲Aⅴ香蕉| 亚洲人成亚洲人成在线观看| 久久亚洲一区二区| 亚洲精品在线视频观看| 亚洲人成网男女大片在线播放| 亚洲人成网亚洲欧洲无码| 欧美亚洲精品一区二区| 亚洲男人的天堂在线va拉文| 亚洲色婷婷一区二区三区| 亚洲国产精品国自产电影| 亚洲制服丝袜在线播放| 亚洲美国产亚洲AV| 亚洲精品视频免费| 精品亚洲综合在线第一区| 精品日韩亚洲AV无码| 成人亚洲国产va天堂| 亚洲AV成人精品日韩一区18p| 久久亚洲国产成人精品无码区| 亚洲av永久无码精品网站| 亚洲视频一区在线|