### 為什么開發者不愿意重構他們的程序?
假設你是一位軟件開發者。如果你的項目剛剛開始(沒有向下兼容的問題),如果你知道系統想要解決的問題,如果你的投資方愿意一直付錢直到你對結果滿意,你真夠幸運。雖然這樣的情景適用面向對象技術,但對我們大多數人來說,這是夢中才會出現的情景。
更多時候,你需要對既有軟件進行擴展,你對自己所做的事情沒有完整的了解,你受到生產進度的壓力。這種情況下你該怎么辦?
你可以重寫整個程序。你可以倚賴自己的設計經驗來糾正程序中存在的錯誤,這是創造性的工作,也很有趣。但誰來付錢呢?你又如何保證新的系統能夠完成舊系統所做的每一件事呢?
你可以拷貝、修改現有系統的一部分,以擴展它的功能。這看上去也許很好,甚至可能被看做一種復用(reuse)方式:你甚至不必理解自己復用的東西。但是,隨著 時間流逝,錯誤會不斷地被復制、被傳播,程序變得臃腫,程序的當初設計開始腐敗變質,修改的整體成本逐漸上升。
重構是上述兩個極端的中庸之道。通過「重新組織軟件結構」,重構使得設計思路更詳盡明確。重構被用于開發框架、抽取可復用組件、使軟件架構〔architecture)更清晰、使新功能的增加更容易。重構可以幫助你充分利用以前的投資,減少重復勞動、使程序更簡化更有性能。
假設你是一位開發者,你也想獲得這些好處。你同意Fred Brooks 所說的「應對并處理變化,是軟件開發的根本復雜性之一」⑵。你也同意,就理論而言,重構能夠提供上面所說的各種好處。
為什么還不肯重構你的程序呢?有幾個可能的原因:
1.
你不知道如何重構。
1.
如果這些利益是長遠(才展現)的,何必現在付出這些努力呢?長遠看來,說不定當項目收獲這些利益時,你已經不在職位上了。
1.
代碼重構是一項額外工作,老板付錢給你,主要是讓你編寫新功能。
1.
重構可能破壞現有程序。
這些擔憂都很正常,我經常聽到電信公司和其他高科技公司的員工那么說。這其中有一些技術問題,以及一些管理問題。首先必須解決所有這些問題,然后開發者才會考慮在他們的軟件中使用重構技術。現在讓我們逐一解決這些問題。
如何重構,在哪里重構
如何才能學會重構呢?有什么工具?有什么技術?如何把這些工具和技術組合起來做出有用的事?應該何時使用它們?本書定義了好幾十條重構作法,這些都是Martin 在自己的工作經驗中發掘的有用手法。重構如何被用以支持程序重大修改?本書提供了很好的例子。
在伊利諾斯大學的軟件重構項目中,我們選擇了一條「極簡抽象派藝術家」(minimalist )路線。我們定義了較少的一組重構⑴,⑶,展示它們的使用方法。我 們對重構的收集系建立于自己的編程經驗上。我們評估好幾個面向對象框架(多數以C++開發完成)的結構演化( structural evolution ),和數字經驗豐富的Smalltalk 開發者交談,并閱讀他們的回顧記錄。我們收集的重構手法大多很低層,例如建立或刪除一個class、一個變量或一個函數,修改變量和函數的屬性,如訪問權限(public 或protected),修改函數參數等等,或者在classes 之間移動變量和函數。我們以另一組數量較少的高級重構手法來處理較為復雜的情況,例如建立abstract superclass、 通過subclassing 和「簡化條件」等方式來簡化一個class 、從現有的class 中分解一 部分,創建一個嶄新而可復用的組件等等(經常會在繼承(inheritance)、委 托(delegation)、聚合(aggregation)之間轉換)。這些較復雜的重構手法是以低層重構手法定義出來的。之所以采用這種方法,乃是為了「自動化支持」和「安全」兩方面考量,我將于稍后討論。
面對一個既有程序,我們該使用哪些重構呢?當然,這取決于你的目標。一個常見的重構原因,同時也是本書關注焦點,是「調整程序結構以使(短期內)添加新功能更容易」。我將在下一節討論這一點,除此之外,還有其他理由讓你使用重構。
有經驗的面向對象程序員和那些受過設計模式(design patterns)和優秀設計技巧訓練的人都知道,目前已經出現數種令人滿意的程序結構性質量和特征(structural qualities and characteristics ),可以支持擴展性和復用性[4],[5],[6]。諸如CRC[7]之類的面向對象設計技術也關注定義classes 和classes 之間的協議(protocols)。雖然它們關注的焦點是前期設計,但也可以用這些指導方針來評價一個現有程序。
自動化工具可用來識別程序中的結構缺陷,例如函數參數過多、函數過長等等。這些都應該考慮成為重構的對象。自動化工具還可以識別出結構上的相似,這樣的相似很可能代表著冗余代碼的存在。比如說,如果兩個函數幾乎相同(這經常是「拷貝/修改」第一個函數以獲得第二個函數時造成的),自動化工具就會檢測到這樣相似性,并建議你使用一些重構手法,將相同代碼搬到同一個地方去。如果程序中不同位置的兩個變量有相同名稱,有時你可以使用一個變量替代它們,并在兩處繼承之。這些都是非常簡單的例子。有了自動化工具,其他很多更復雜的情況都可以被檢測出來并被糾正。這些結構上的畸形或結構上的相似并非總是暗示你必須重構, 但很多時候它們的確就是這個意思。
對設計模式(design patterns)的很多研究,都集中于良好編程風格以及程序各部位之間有用的交互模式(patterns of interactions ),而這些都可以映像為結構特征和重構手法。例如Template Method 模式[8]的「適用性」(applicability)一節就參考 了我們的abstract superclass 重構手法[9]。
我列出了一些試探法則[1],可以幫助你識別C++程序中需要重構的地方。John Brant 和 Don Roberts[10],[11]開發出一個工具,使用更大范圍的試探來自動分析Smalltalk 程序。這個工具會向幵發者建議「可用以改進程序」的重構方法,以及適合使用這些重構方法的地點。
運用這樣一個工具來分析你的程序,有點像運用lint 來改善C/C++程序。這個工具尚未聰明到能夠理解程序意圖,它在程序結構分析基礎上提出的建議,或許只有一部分是你真正想要做出的修改。作為程序員,決定權在你手上。由你決定把哪些建議用于自己的程序上。這些修改應該改進程序的結構,應該為日后的修改提供更好的支撐。
在程序員說服自己「我應該重構我的代碼」之前,他們需要先了解如何重構、在哪里重構。經驗是無可替代的。研究過程中,我們得益于經驗豐富的面向對象開發者的經驗,得到了一些有用的重構作法,以及「該在哪里使用這些重構」的認識。自動化工具可以分析程序結構,建議可能改進程序結構的重構作法。和其他大多數學科一樣,工具和技術會帶來幫助,但前提是你打算使用它們。重構過程中,程序員自己對重構的理解也會逐漸加深。
重構 C++ 程序
Bill Opdyke
1989.年,我和Ralph Johnson 剛開始研究重構的時候,C++ 正在飛快發展,并日漸在面向對象開發圈中流行起來。Smalltalk 用戶是最先汄識重構重要性的一群人,而我們認為,如果能夠證明重構對C++ 程序也同樣可用,就會使更多面向對象開發者對重構產生興趣。
C++ 的某些語言特性(特別是靜態型別檢查)簡化了一部分程序分析和重構工作。但是另一方面,C++ 語言很復雜也很龐大,這很大程度是由于其歷史而造成(C++ 是從C 語言演化而來的)。C++ 允許的某些編程風格,使程序的重構和發展變得困難。
對重構有支持能力的語言特性和編程風袼
重構吋,你必須找出待重構的這一部分程序被什么地方引用(指涉)。C++ 靜態型別特性讓你可以比較容易地縮小搜索范圍。舉個簡單但常見的例子,假設你想要給C++ class 的一個成員函數改名,為正確完成這個動作,你必須修改函數聲明以及對這個函數的所有引用點。如果程序很大,搜索、修改這些引用點會很困難。
和Smalltalk 相比,C++ 的classes 繼承和保護訪問級別(public、protected和private )特性,使你更容易判斯哪些地方引用了這個「待易名函數」,如果這個函數被其所屬class 聲明為private ,那么這個函數的「被引用點」就只可能出現在這個class 內部以及被這個class 聲明為friend 的地方;如果這個函數被聲明為protected ,那么引用點只可能出現在它所屬的class 內、它的subclass (及更底層的subclass )內以及它的friends 中:如果這個函數被聲明為public (限制最少的一種訪問級別),引用點彼限制在上述protected 所列情況,以及對某些特定class 實體(對象)的操作之上——該特定class 可以是內含「待易名函數」者,或其subclasses,或更底層的subclasses。
在十分龐大的程序中,不同地點有可能聲明一些同名函數。有時候,兩個或多個同名函數以同一個函數取代可能更好,某些重構手法可用來做這種修改;有時候則應該給兩個同名函數中的一個改名,讓另一個保持原來名稱。如果項目開發成員不只一人,不同的程序員可能給風牛馬不相及及的函數取相同的名稱。在C++ 中當你對兩個同名函數中的一個改名之后,幾乎總是很容易找到哪些引用點針對的是這個被易名函數,哪些引用點針對的是另一個函數。這種分析在Smalltalk 中要困難得多。
由于C++ 以subclassing 實現subtyping,所以通常可以通過「將變量或函數在繼承體系中移上移下」來擴大(普通化)I或縮小(特殊化)其作用域(scope)。對程序做這一類分析并進行相應重構,都是很簡單的。
如果在最初開發和整個開發過程中一直遵循一些良好的設計原則,那么重構過程會更輕松,軟件的進化會更容易。「將所有成員變量和大多數成員函數定義為private 或protected 」是一個抽象技術,常常使class 的內部重構更簡單,因為對程序其他地方造成的影響被減至最低。以繼承機制表現「普通化和特殊化」體系(這在C++ 中很自然),也使日后「泛化或特化成員變量或成員函數」的重構動作更容易進行,你只需在繼承體系內上下移動這些成員即可。
C++ 環境中的很多特性都支持重構。如果程序員在重構時引入錯誤,C++ 編譯器通常都會指出這個錯誤。許多C++ 軟件開發環境都提供了強大的交叉參考和代碼瀏覽功能。
增加重構復雜度的語言特性和編程風格
眾所周知,C++ 對C 的兼容性是一柄雙刃劍。許多程序以C 寫成,許多程序員受的訓練是C 風格,所以(至少從表面看來)轉移到C++ 比轉移到其他面向對象語言容易些。 但是支持許多編程風格,其中某些違反了合理健全的設計原則。
程序如果使用諸如指針、轉型操作(cast operation)和sizeof(object)之類的C++ 特性,將難以重構。指計和轉型搡作會造成別名(alias),使你很難找到待重構對象的所有被引用點。上述這些特性暴露了對象的內部表現形式,違反了抽象原則。
舉個例子,在可執行程序中,C++ 以V-table 機制表現成員變量。從superclass 繼承而來的成員變量首先出現,而后才是自身(locally)定義的成員變量。「將某個變量移往superclass 」通常是很安全的重構手法,但如果變量是由superclass 繼承而來,不是subclass 自身定義出來,它在可執行文件中的物理(實際)位置有可能因這樣的重構而發生改變。當然啦,如果程序中對變量的所有引用(指涉)都是通過class interface 進行,變量的物理位置調整,并不會改變程序行為。
此外,如果程序通過指針算術運算來引用這個變量(例如程序員擁有一個對象指針,而且他知道他想賦值的變量保存于第5個byte ,于是他就使用指針算術,直接把一個值賦進對象的第5個byte 去),那么「將變量移到superclass 」的重構手法就有可能改變程序行為。同樣地,如果程序員寫下 if (sizeof(object) == 15) 這樣的條件式,然后又對程序進行重構,刪除class 之中未用到的變量,那么這個class 的實體大小就會發生改變,導致先前判斷為真的條件式,如今有可能判斷為偽。
可曾有人根據對象大小做條件判斷?C++ 提供遠為清楚的接口用以訪問成員變量,還會有人以指針運算進行訪問嗎?這樣寫程序實在太荒唐了不是嗎?我的觀點是:C++ 提供了這些特性(以及其他倚賴對象物理布局的特性),而某些經驗豐富的程序員的確使用了它們。畢竟,從C 到C++ 的移植不可能由面向對象程序員或設計師來進行(只能由C 程序員來做)。
由于C++ 是一個如此復雜的語言(和Smalltalk 以及Java 相比),意圖建立某種程序結構,使之得以「協助自動檢查某一重構是否安全,并于安全情況下自動執行該重構」,就困難得多。
C++ 在編譯期對大多數references 進行決議(resolves),所以對一個C++ 程序進行重構,通常需要至少重新編譯程序的某一部分,重新連接并生成可執行文件,然后才能測試修改效果。與之形成鮮明對比的是,Smalltalk 和 CLOS (Common Lisp Object System) 提供解釋(interpretation )和增量編譯(incremental compilation)環境。因此盡管在Smalltalk 和CLOS 中進行一系列漸進式重構是很自然的事,對C++ 程序來說,每次迭代(重新編譯 + 測試)的成本卻太高了,所以C++ 程序員往往不太樂意經常做這種小改動。
許多應用程序都用到了數據庫。如果在C++ 程序中改變對象結構,可能會需要對database schma(數據庫表格的結構、架構、定義)作相應修改。(我在重構工作中應用的許多思想都來自對面向對象數據庫模型演化的研究。)
C++ 的另一個局限性(這對軟件研究者的吸引力可能大于軟件開發者)就是:它沒有支持meta-level 的程序分析和修改。C++ 缺乏任何類似CLOS metaobject 協議的東西。舉個例子,CLOS 的metaobject 協議支持一個時而很有用的重構手法:將選定的對象變成另一個class 的實體,并讓所有指向就對象的references 自動指向新對象。幸運的是只有在極少數情況下才會需要這種特性。
結語
很多時候,重構技術可以(并且已經)應用于C++ 程序了,C++ 程序員通常希望自己的程序能在未來數年中不斷演化進步,而軟件演化過程正是最能凸顯重構的好處。C++ 語言提供的某些特性可以簡化重構,但另一些特性會使重構變得困難。幸運的是,程序員已經公認:使用諸如「指針運算」之類的語言特性并不是好主意。大多數優秀的面向對象程序員都會避免使用它們。
非常感謝Ralph Johnson, Mick Murphy, James Roskind 以及其他一些人,向我介紹了C++ 之于重構的威力和復雜性。
重構以求短期利益
要說明「重構有哪些中長期好處」是比較容易的。但許多公司受到來自投資方日益沉重的壓力,不得不追求短期成績。重構可以在短期之內帶來驚喜嗎?
那些經驗豐富的面向對象開發者,成功運用重構已經有超過十年的歷史了。在強調代碼簡潔明了、復用性高的Smalltalk 文化中,許多程序員都變得成熟了。在這樣的文化中,程序員會投入時間去進行重構,因為他應該這樣做。Smalltalk 語言和實現品使得重構成為可能,這是過去絕大多數語言和開發環境都沒有能夠做到的。許多早期的Smalltalk 程序設計都是在Xerox、PARC 這樣的研究機構或技術尖端的小型開發團隊和顧問公司中進行的。這些團體的價值觀和許多產業化軟件團隊的價值觀是有所差異的。Martin 和我都知道:如果要讓主流軟件開發者接受重構思想, 重構帶來的利益起碼有一部分必須能夠在短期內體現出來。
我們的研究團隊[3], [9], [12], [13], [14], [15] 記錄了數個例子,描述重構如何和程序功能的擴展交錯進行,最終同時獲得短期利益和長期利益。我們的一個例子是Choices 文件系統框架。最初這個框架實現了 BSD (Berkeley Software Distribution) Unix 文件系統格式。后來它又被擴展支持UNIX System V, MS-DOS、永續性(persistent )和分布式(distributed)文件系統。框架開發者采用的辦法是:先把實現BSD Linux 的部分原樣復制一份過來,然后修改它,使它支持System V。系統最終可以有效運作,但充斥大量重復的代碼。加入新代碼后,框架開發者重構了這些代碼,建立abstract superclass 容納兩個Unix 文件系統的共通行為。相同的變量和函數被移到superclass 中。當兩個對應函數幾乎相同、但不完全相同時,他們就在subclass 中定義新函數來包容兩者不同之處,然后在原先函數里頭把這些代碼換成對新函數的調用。這樣一來,兩個subclass 的代碼就逐漸變得愈來愈相似了。一旦兩個函數變得完全相同,就可以將它們搬移到共同的superclass 去。
這些重構手法為開發者提供了多方面好處,既有短期利益,也有長期利益。短期來看,如果在測試階段發現共同的代碼有錯誤,只需在一個地方修改就行了。代碼總量變少了。「特定于某一文件系統的行為」與「兩種文件系統的共同行為」清晰地分開了,這使得追蹤、修補「特定于某種文件系統的行為」更加容易。中期來看,重構得到的抽象層對于定義后續文件系統常常很有幫助。當然,現有的兩種文件系統的共通行為未必就完全適用于第三種文件格式,但現有的共享基礎是一個很有價值的起點。后繼的重構動作可以澄清究竟哪些東西真正是所有文件系統共有的。框架開發團隊發現:隨著時間流逝,「增加新文件系統的支持」愈來愈省勁。就算新的格式更復雜、開發團隊經驗更淺,情況也一樣。
我還可以找出其他例子來證明重構能夠帶來短期和長期利益,但是Martin 早已做了 此事,我不想再延長他的列表。還是拿我們都非常熟悉的一件事來做個比喻吧:我 們的身體健康狀況。
從很多角度來說,重構就好像運動、吃適當的食物。許多人都知道:我們應該多鍛煉身體,應該注意均衡飲食。有些人的生活文化中非常鼓勵這些習慣,有些人沒有這些好習慣也可以混過一段時間,甚至看不出有什么影響。我們可以找各種借口, 但如果一直忽視這些好習慣,那么我們只是在欺騙自己。
有些人之運動和均衡飲食,動機著眼于短期利益(例如精力更充沛、身體更靈活、 自尊心增強……等等)。幾乎所有人都知道這些短期利益非常真實。許多人(但不是所有人)都時斷時續做過一些努力,另一些人則是不見棺材不掉淚,不到關鍵時 刻不會有足夠動力去做點什么事。
沒錯,做事應該謹慎。在著手干一件事之前,應該先向專家咨詢一下。在開始運動和均衡飲食之前,應該先問問自己的保健醫生。在開始重構之前,應該先查找相關資源——你手上這本書和本章引用的其他數據都很好。對重構有豐富經驗的人可以 向你提供更到位的幫助。
我見過的一些人正是「健康與重構」的典范。我羨慕他們旺盛的精力和超人的工作性能。反面典型則是明顯的粗心大意愛忘事,他們的未來和他們開發的軟件產品的未來,恐怕都不會很光明。
重構可以帶來短期利益,讓軟件更易修改、更易維護。重構只是一種手段,不是目的。它是「程序員或程序開發團隊如何開發并維護自己的軟件」這一更寬廣場景的一部分⑶。
降低重構帶來的額外幵銷(Reducing the Overhead of Refactoring)
『重構是一種需要額外開銷的活動。我付錢是為了讓程序員寫出新的、能帶來收益的軟件功能』。對于這種聲音,我的回復總結如下:
- 目前已有一些工具和技術,可以使重構「快速」而「相對無痛苦」地完成。
- 一些面向對象程序員的經驗顯示,重構雖然需要額外開銷,但可以從它「在程序開發的其他階段協助降低所需心力及滯怠時間」而獲得補償。
- 盡管乍見之下重構可能有點笨拙、開銷太大,但是當它成為軟件開發規則的一部分,人們就不會再覺得它費事,反而開始覺得它是必不可少的。
- 伊利諾斯大學的軟件重構團隊開發的Smalltalk 自動化重構工具也許是目前最成熟的自動化重構工具〔參見第14章〉。你可以從他們的網站([http://st-www.cs.vivc.edu)自由下載這個工具。盡管其他語言的重構工具還沒能這么方便,但是我們的論文和本書介紹的許多技術,都可以相對簡單地套用,只要有一個文本編輯器或一個瀏覽器就足夠了。軟件開發環境和瀏覽器技術已經在最近數年獲得了長足發展。我們希望將來能看到更多重構工具投入使用。](http://st-www.cs.vivc.edu%EF%BC%89%E8%87%AA%E7%94%B1%E4%B8%8B%E8%BD%BD%E8%BF%99%E4%B8%AA%E5%B7%A5%E5%85%B7%E3%80%82%E5%B0%BD%E7%AE%A1%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%9A%84%E9%87%8D%E6%9E%84%E5%B7%A5%E5%85%B7%E8%BF%98%E6%B2%A1%E8%83%BD%E8%BF%99%E4%B9%88%E6%96%B9%E4%BE%BF%EF%BC%8C%E4%BD%86%E6%98%AF%E6%88%91%E4%BB%AC%E7%9A%84%E8%AE%BA%E6%96%87%E5%92%8C%E6%9C%AC%E4%B9%A6%E4%BB%8B%E7%BB%8D%E7%9A%84%E8%AE%B8%E5%A4%9A%E6%8A%80%E6%9C%AF%EF%BC%8C%E9%83%BD%E5%8F%AF%E4%BB%A5%E7%9B%B8%E5%AF%B9%E7%AE%80%E5%8D%95%E5%9C%B0%E5%A5%97%E7%94%A8%EF%BC%8C%E5%8F%AA%E8%A6%81%E6%9C%89%E4%B8%80%E4%B8%AA%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8%E6%88%96%E4%B8%80%E4%B8%AA%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B0%B1%E8%B6%B3%E5%A4%9F%E4%BA%86%E3%80%82%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E5%92%8C%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8A%80%E6%9C%AF%E5%B7%B2%E7%BB%8F%E5%9C%A8%E6%9C%80%E8%BF%91%E6%95%B0%E5%B9%B4%E8%8E%B7%E5%BE%97%E4%BA%86%E9%95%BF%E8%B6%B3%E5%8F%91%E5%B1%95%E3%80%82%E6%88%91%E4%BB%AC%E5%B8%8C%E6%9C%9B%E5%B0%86%E6%9D%A5%E8%83%BD%E7%9C%8B%E5%88%B0%E6%9B%B4%E5%A4%9A%E9%87%8D%E6%9E%84%E5%B7%A5%E5%85%B7%E6%8A%95%E5%85%A5%E4%BD%BF%E7%94%A8%E3%80%82/)
Kent Beck 和 Ward Cunningham 都是經驗豐富的Smalltalk 程序員,他們已經在OOPSLA 和其他論壇上提出報告:重構使他們能夠更快開發證券交易之類的軟件。 從C++ 和CLOS 開發者那里,我也聽到了同樣的消息。本書之中Martin 介紹了重構對于程序的好處。我們希望讀過本書、使用書中介紹的重構原則的人們,能夠給我們帶來更多好消息。
從我的經驗看來,只要重構成為日常事務的一部分,人們就不會覺得它需要多么高昂的代價。說來容易做來難。對于那些懷疑論者,我的建議就是:只管去做,然后自己決定。但是,請給它一點時間證明它自己。
安全地進行重構
安全性(safety)是令人關心的議題,特別對于那些開發、維護大型系統的組織更是如此。許多應用程序背負著財政、法律和道德倫理方面的壓力,必須提供不間斷的、可靠的、不出錯的服務。有許多組織提供大量培訓和努力,力圖以嚴謹的開發過程來幫助他們保證產品的安全性。
但是,對很多程序員來說,安全性的問題往往沒那么嚴重。我們總是向孩子們灌輸 「安全第一」的思想,自己卻扮演渴望自由的程序員、西部牛仔和血氣方剛的駕駛員的角色,這實在是個莫大諷刺。給我們自由,給我們資源,看我們飛吧。不管怎 么說,難道我們真的希望公司放棄我們的創造性果實,就為了獲得可重復性和一致性嗎?
這一節我將討論安全重構(safe refactoring)的方法。和Martin 在本書先前章節介 紹過的方法相比,我關注的方法其結構比較更組織化、更嚴格,可因此排除重構可能引入的很多錯誤。
安全性(safety)是一個很難定義的概念。直觀的定義是:所謂「安全重構」(safe refactoring)就是不會對程序造成破壞的重構。由于重構的意圖就是在不改變程序行為的前提下修改程序結構,所以重構后的程序行為應該與重構前完全相同。
如何進行安全重構呢?你有以下數種選擇:
- 相信你自己的編碼功力。
- 相信你的編譯器能捕捉你遺漏的錯誤。
- 相信你的測試套件(test suite )能捕捉你和編譯器都遺漏的錯誤。
- 相信代碼復審(code review)能捕捉你、編譯器和測試套件(test suite )都遺漏的錯誤。
Martin 在他的重構原則中比較關注前三個選項。大中型公司則常常以代碼復審作為前三個步驟的補充。
盡管編譯器、測試套件、代碼復審、嚴守紀律的編碼風格都很有價值,但所有這些方法還是有下列局限性:
- 程序員是可能犯錯的,你也一樣(我也一樣)。
- 有一些微妙和不那么微妙的錯誤,編譯器無法捕捉,特別是那些與繼承相關的作用域錯誤(scoping errors)[1]。
- Perry and Kaiser[16] 和其他人已經指出,盡管「將繼承作為一種實現技術」的作 法讓測試工作簡單了不少,但由于先前「向class 的某個實體發出請求」的很多操作如今「轉而向subclass 發出請求」,我們仍然需要大量測試來覆蓋這種情況。除非你的測試設計者是全知全能的上帝,或除非他對細節非常謹慎,否則就有可能出現測試套件禝蓋不到的情況。「是否測試了所有可能的執行 路徑」?這是一個無法以計算判定的問題。換句話說,你無法保證測試套件覆蓋所有可能情況。
- 和程序員一樣,代碼復審人員也是可能犯錯的。而且復審人員可能因為忙于自己的主要工作,無法徹底檢杳別人的代碼。
我在研究工作中使用的另一種方法是:定義并快速實現一個重構工具前原型,用以檢查某項重構是否可以安全地施加于程序身上。如果可以,就重構之。這避免了大量可能因為人為錯誤而引入的臭蟲。
在這里,我將概括介紹我的安全重構(safe refactoring)法。這可能是本章最具價值的一部分了。如果你想獲得更詳細的信息,請看我的論文[1]和本章末尾所列的參考文獻,也可以參考本書第14章。如果你覺得這一部分有點過分偏重技術,不妨跳過本節余下的數小段。
我的重構工具的一部分是程序分析器(program analyzer),這是一個用來分析程序結構的程序(被分析的對象是將來打算施加某項重構的一個C++ 程序)。這個工具可以解答一系列問題,內容涉及作用域(scoping)、型別(typing)和程序語義 (程序的意圖或用途)等方面。作用域的問題與繼承有關,所以這一分析過程比起很多「非面向對象程序分析」要復雜;但的某些語言特性(例如靜態型別,static typing)又使得這一分析過程比起「對Smalltalk 等動態型別(dynamic typing ) 程序的分析」要簡單。
舉個例子,假設我們的重構是要刪除程序中的某個變量。我的工具可以判斷程序其他部分(如有的話)是否引用了這個變量。如果有,徑自刪除這一變量將會造成dangling references,那么這項重構就是不安全的。于是工具用戶就會收到一個錯誤標記(error flag)。用戶可能因此決定放棄進行這次重構,也可能修改程序中對此變量的引用點,使它們不再引用它,然后才進行重構,刪除該變量。這個工具還可以進行其他許多檢查,其中大多數都和上述檢查一樣簡單,有些稍微復雜。
在我的研究中,我把安全(safety)定義為:「程序屬性(包括作用域和型別等等) 在重構之后仍然保持不變」。很多程序屬性很像數據庫中的完整性約束(integrity constraints )——修改database schemas (數據庫表格的結構、架構、定義)時,完整性約束必須保持不變[17]。每個重構都伴隨一組必要前提,如果這些前提得到滿足,該重構就能保證程序屬性獲得維持。一旦確定某次重構的全部過程都安全,我的工具才會執行該次重構。
幸運的是,對于「重構是否安全」進行的檢查(尤其是對于數量占絕對優勢的低層重構〕往往是瑣屑而平淡無奇的。為了保證較高層重構、較復雜重構的安全性, 我們以低層重構來定義它們。例如「建立一個abstract superclass 」的復雜重構手法就被定義為數個較小步驟,每個步驟都以較簡單的重構完成,像是創建和搬移變量或函數等等。只要證明復雜重構的每一個步驟是安全的,我們就可以確定整個復雜 重構也是安全的。
在某些十分罕見的情況下,重構其實可以在「工具無法確認」時仍然安全施加于程序身上。在那種情況下,工具會選擇較安全的方式:禁止重構。拿先前例子來說,你想刪除程序中的某個變量,但程序其他地方對該變量有引用動作。然而或許這個引用動作所處段落永遠不會被執行到,例如它也許出現于條件式(如if - then)中, 而它所處分支永遠不為真。如果肯定這個分支永遠不為真,你可以移除它,連同那個影響你重構的引用點一并移除。然后你就可以安全地進行重構,刪除你想刪除的變量或函數了。只不過,一般情況下你無法肯定分支永遠為假(如果你繼承了別人開發的代碼,你有多大把握安全刪掉其中某段代碼?)
重構工具可以標記出這種「可能不安全」的引用關系,并向用戶提出警告。用戶可以先把這段代碼放在一旁。一旦用戶能夠肯定引用點永遠不會被執行到時,他就可以把這段多余代碼移除,而后進行重構。這個工具讓用戶知道存在這么一個隱藏的引用關系,而不是盲目地進行修改。
這聽起來好像有點復雜,作為博士論文的主題倒是不錯(博士論文的主要讀者——論文評議委員會——比較喜歡理論性題目),但是對于實際重構有用嗎?
所有這些安全性檢查都可以在重構工具中實現。如果程序員想要重構一個程序,只需以這個工具檢查其代碼。如果檢查結果為「安全」,就執行重構。我的工具只是個研究雛型。Don Roberts, John Brant, Ralph Johnson 和我[10]后來實現了一個體質更健壯、功能更齊備的工具〔參見第14章),這是我們對于「Smalltalk 程序重構」 研究的一部分。
安全性可分很多不同級別施行于重構身上。其中某些級別容易實施,但不保證高級安全性。使用重構工具有很多好處。它可以在高級問題中做許多簡單而乏味的檢查和標記。如果不做這些檢查,重構動作有可能導致程序完全崩潰。
編譯、測試和代碼復審可以指出很多錯誤,但也會遺漏一些錯誤,重構工具則可以幫助你抓住漏網之魚。盡管如此,編譯、測試和代碼復審仍然是很有價值的,在實時(real-time)系統的開發和維護中更是如此。這些系統中的程序往往不是孤立運行的,它們是大型通信系統網絡中的一部分。有些重構不但把代碼清掃干凈,而且會讓程序跑得更快。然而提升某個程序的速度,可能會在另一個地方造成性能瓶頸。 這就好像你升級CPU 進而提升了部分系統性能,你需要以類似方法來調整、測試系統整體性能。另一方面,有些重構也可能略微降低系統整體性能。一般說來,重構對性能的影響是微不足道的。
「安全性作法」用來保證重構不會向程序引入新錯誤。這些作法并不能檢查或修復 程序重構前就存在的錯誤。但重構可以使你更容易找到并修復這些錯誤。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目