### 重構的難題
學習一種可以大幅提高生產力的新技術時,你總是難以察覺其不適用的場合。通常你在一個特定場景中學習它,這個場景往往是個項目。這種情況下你很難看出什么會造成這種新技術成效不彰或甚至形成危害。十年前,對象技術(object tech.)的情況也是如此。那時如果有人問我「何時不要使用對象」,我很難回答。并非我認為對象十全十美、沒有局限性——我最反對這種盲目態度,而是盡管我知道它的好處,但確實不知道其局限性在哪兒。現在,重構的處境也是如此。我們知道重構的好處,我們知道重構可以給我們的工作帶來垂手可得的改變。但是我們還沒有獲得足夠的經驗,我們還看不到它的局限性。
這一小節比我希望的要短。暫且如此吧。隨著更多人學會重構技巧,我們也將對它有更多了解。對你而言這意味:雖然我堅決認為你應該嘗試一下重構,獲得它所提供的利益,但在此同時,你也應該時時監控其過程,注意尋找重構可能引入的問題。請讓我們知道你所遭遇的問題。隨著對重構的了解日益増多,我們將找出更多解決辦法,并清楚知道哪些問題是真正難以解決的。
**數據庫(Database)**
「重構」經常出問題的一個領域就是數據庫。絕大多數商用程序都與它們背后的database schema(數據庫表格結構)緊密耦合(coupled〕在一起,這也是database schema如此難以修改的原因之一。另一個原因是數據遷移(migration)。就算你非常小心地將系統分層(layered),將database schema和對象模型(object model)間的依賴降至最低,但database schema的改變還是讓你不得不遷移所有數據,這可能是件漫長而煩瑣的工作。
在「非對象數據庫」(nonobject database)中,解決這個問題的辦法之一就是:在 對象模型(object model)和數據庫模型(database model)之間插入一個分隔層 (separate layer),這就可以隔離兩個模型各自的變化。升級某一模型時無需同時升級另一模型,只需升級上述的分隔層即可。這樣的分隔層會增加系統復雜度,但可以給你很大的靈活度。如果你同時擁有多個數據庫,或如果數據庫模型較為復雜使你難以控制,那么即使不進行重構,這分隔層也是很重要的。
你無需一幵始就插入分隔層,可以在發現對象模型變得不穩定時再產生它。這樣你就可以為你的改變找到最好的杠桿效應。
對開發者而言,對象數據庫既有幫助也有妨礙。某些面向對象數據庫提供不同版本的對象之間的自動遷移功能,這減少了數據遷移時的工作量,但還是會損失一定時間。如果各數據庫之間的數據遷移并非自動進行,你就必須自行完成遷移工作,這個工作量可是很大的。這種情況下你必須更加留神classes內的數據結構變化。你仍然可以放心將classes的行為轉移過去,但轉移值域(field)時就必須格外小心。數據尚未被轉移前你就得先運用訪問函數(accessors)造成「數據已經轉移」的假象。一旦你確定知道「數據應該在何處」時,就可以一次性地將數據遷移過去。這時惟一需要修改的只有訪問函數(accessors),這也降低了錯誤風險。
**修改接口(Changing Interfaces)**
關于對象,另一件重要事情是:它們允許你分開修改軟件模塊的實現 (implementation〕和接口(interface)。你可以安全地修改某對象內部而不影響他人,但對于接口要特別謹慎——如果接口被修改了,任何事情都有可能發生。
一直對重構帶來困擾的一件事就是:許多重構手法的確會修改接口。像Rename Method這么簡單的重構手法所做的一切就是修改接口。這對極為珍貴的封裝概念會帶來什么影響呢?
如果某個函數的所有調用動作都在你的控制之下,那么即使修改函數名稱也不會有任何問題。哪怕面對一個public函數,只要能取得并修改其所有調用者,你也可以安心地將這個函數易名。只有當需要修改的接口系被那些「找不到,即使找到也不能修改」的代碼使用時,接口的修改才會成為問題。如果情況真是如此,我就會說:這個接口是個「已發布接口」(published interface)——比公開接口(public interface)更進一步。接口一旦發布,你就再也無法僅僅修改調用者而能夠安全地修改接口了。 你需要一個略為復雜的程序。
這個想法改變了我們的問題。如今的問題是:該如何面對那些必須修改「已發布接口」的重構手法?
簡言之,如果重構手法改變了已發布接口(published interface〕,你必須同時維護新舊兩個接口,直到你的所有用戶都有時間對這個變化做出反應。幸運的是這不太 困難。你通常都有辦法把事情組織好,讓舊接口繼續工作。請盡量這么做:讓舊接口調用新接口。當你要修改某個函數名稱時,請留下舊函數,讓它調用新函數。千萬不要拷貝函數實現碼,那會讓你陷入「重復代碼」(duplicated code)的泥淖中難以自拔。你還應該使用Java提供的(deprecation〕設施,將舊接口標記為 "deprecated"。這么一來你的調用者就會注意到它了。
這個過程的一個好例子就是Java容器類(群集類,collection classes)。Java2的新容器取代了原先一些容器。當Java2容器發布時,JavaSoft花了很大力氣來為開發者提供一條順利遷徙之路。
「保留舊接口」的辦法通常可行,但很煩人。起碼在一段時間里你必須建造(build)并維護一些額外的函數。它們會使接口變得復雜,使接口難以使用。還好我們有另 一個選擇:不要發布(publish)接口。當然我不是說要完全禁止,因為很明顯你必得發布一些接口。如果你正在建造供外部使用的APIs,像Sun所做的那樣,肯定你必得發布接口。我之所以說盡量不要發布,是因為我常常看到一些開發團隊公開了太多接口。我曾經看到一支三人團隊這么工作:每個人都向另外兩人公開發布接口。這使他們不得不經常來回維護接口,而其實他們原本可以直接進入程序庫,徑行修改自己管理的那一部分,那會輕松許多。過度強調「代碼擁有權」的團隊常常會犯這種錯誤。發布接口很有用,但也有代價。所以除非真有必要,別發布接口。這可能意味需要改變你的代碼擁有權觀念,讓每個人都可以修改別人的代碼,以運應接口的改動。以搭檔(成對〕編程(Pair Programming)完成這一切通常是個好主意。
TIP:不要過早發布(publish)接口。請修改你的代碼擁有權政策,使重構更順暢。
Java之中還有一個特別關于「修改接口」的問題:在Throws子句中增加一個異常。這并不是對簽名式(signature)的修改,所以你無法以delegation(委托手法)隱 藏它。但如果用戶代碼不做出相應修改,編譯器不會讓它通過。這個問題很難解決。你可以為這個函數選擇一個新名字,讓舊函數調用它,并將這個新增的checked exception(可控式異常〗轉換成一個unchecked exception(不可控異常:)。你也可 以拋出一個unchecked異常,不過這樣你就會失去檢驗能力。如果你那么做,你可以警告調用者:這個unchecked異常日后會變成一個checked異常。這樣他們就有時間在自己的代碼中加上對此異常的處理。出于這個原因,我總是喜歡為整個package定義一個superclass異常(就像java.sql的SQLException),并確保所有public函數只在自己的throws子句中聲明這個異常。這樣我就可以隨心所欲地定義異常,不會影響調用者,因為調用者永遠只知道那個更具一般性的superclass異常。
**難以通過重構手法完成的設計改動**
通過重構,可以排除所有設計錯誤嗎?是否存在某些核心設計決策,無法以重構手法修改?在這個領域里,我們的統計數據尚不完整。當然某些情況下我們可以很有效地重構,這常常令我們倍感驚訝,但的確也有難以重構的地方。比如說在一個項目中,我們很難(但還是有可能)將「無安全需求(no security requirements)情況下構造起來的系統」重構為「安全性良好的〔 good security)系統」。
這種情況下我的辦法就是「先想像重構的情況」。考慮候選設計方案時,我會問自己:將某個設計重構為另一個設計的難度有多大?如果看上去很簡單,我就不必太擔心選擇是否得當,于是我就會選最簡單的設計,哪怕它不能覆蓋所有潛在需求也沒關系。但如果預先看不到簡單的重構辦法,我就會在設計上投入更多力氣。不過我發現,這種情況很少出現。
**何吋不該重構?**
有時候你根本不應該重構——例如當你應該重新編寫所有代碼的時候。有時候既有代碼實在太混亂,重構它還不如重新寫一個來得簡單。作出這種決定很困難,我承認我也沒有什么好準則可以判斷何時應該放棄重構。
重寫(而非重構)的一個清楚訊號就是:現有代碼根本不能正常運作。你可能只是試著做點測試,然后就發現代碼中滿是錯誤,根本無法穩定運作。記住,重構之前,代碼必須起碼能夠在大部分情況下正常運作。
一個折衷辦法就是:將「大塊頭軟件」重構為「封裝良好的小型組件」。然后你就可以逐一對組件做出「重構或重建」的決定。這是一個頗具希望的辦法,但我還沒有足夠數據,所以也無法寫出優秀的指導原則。對于一個重要的古老系統,這肯定會是一個很好的方向。
另外,如果項目已近最后期限,你也應該避免重構。在此時機,從重構過程贏得的生產力只有在最后期限過后才能體現出來,而那個時候已經時不我予。Ward Cunningham對此有一個很好的看法。他把未完成的重構工作形容為「債務」。很多公司都需要借債來使自己更有效地運轉。但是借債就得付利息,過于復雜的代碼所造成的「維護和擴展的額外開銷」就是利息。你可以承受一定程度的利息,但如果利息太高你就會被壓垮。把債務管理好是很重要的,你應該隨時通過重構來償還一部分債務。
如果項目己經非常接近最后期限,你不應該再分心于重構,因為己經沒有時間了。不過多個項目經驗顯示:重構的確能夠提高生產力。如果最后你沒有足夠時間,通常就表示你其實早該進行重構。
- 譯序 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 )
- 小結
- 章節十五 集成
- 參考書目