重構-改善既有程式碼的設計

linlinlinxi007發表於2010-01-27

Erich在《重構》的序言中說到:程式碼被閱讀和被修改的次數遠遠多於他被編寫的次數。保持程式碼的易讀、易修改的關鍵,就是重構。

重構的目的是什麼?書中的第二章有專門的一節作了解釋。不過第一章舉了個簡單案例,展現了一個相對完整的重構過程。這個案例初始一共只有三個類,其中一個類中的一個方法長達五十行左右,一眼望去,就兩個字:不爽。就像書中說的它做的事情實在是太多了即便如此,這個程式還是能正常工作,……編譯器才不會在乎程式碼好不好看呢,但是當我們打算修改系統的時候,就涉及到了人,而人在乎這些。差勁的系統是很難修改的,因為很難找到修改點。如果很難找到修改點,程式設計師就很有可能犯錯,從而引入bug我覺得這就是為什麼要重構的一個重要原因。

一個方法如果過長,說明裡面做了很多事情,涉及到了很多步驟。這樣的話一旦要對功能進行修改,就首先要讀懂長長的一大段程式碼,然後再分解其中的功能,找到此次修改所涉及到的程式碼區等等一系列步驟,這樣會給進一步開發帶來很大的麻煩。而且,分解長的方法,可以幫助我們發現重複程式碼。重複程式碼的副作用恐怕大部分的程式設計人員都瞭解。就在今天,我在重構最近一個月開發的程式碼的時候,首先把一些好幾十行的方法都拆分成小的程式碼段,拆分之後,我發現有兩個類三個方法中,都執行了這樣一段相同的邏輯:根據某個表的一個唯一索引查詢一條記錄。這樣的程式碼完全可以拆分出來,以減少重複程式碼,如果以後庫表結構發生變化,我們還可以只修改一處,否則,真的是改起來太困難了。而如果沒有把長程式碼拆分的話,很難從那麼多冗長的方法中,發現重複程式碼。

 

針對第一章簡單案例,重構的大致過程如下:

 

我們首先拆分長函式,第一個步驟是找出程式碼的logical clump,並運用Extract Method。

另外,要注意使用能清晰表達業務邏輯含義的變數名。好的程式碼應該清楚表達出自己的功能,變數的名稱是程式碼清晰的關鍵。所以更改變數名稱是值得的行為。我就碰到過這樣的程式碼:某個類提供一個對外的public方法,該方法主要功能是根據傳入的一些引數,去資料庫表中查詢一些資訊並返回。在這個方法的引數列表中,引數名使用了需要查詢的庫表的欄位名。我們資料庫表的欄位命名規則是PK/FK+縮寫表名+縮寫實際欄位名,在保證欄位名稱不能過長的原則下,這一路“縮”下來,如果不是對庫表非常熟悉的人,僅僅通過欄位名,很難判斷它的業務含義。我們姑且不論資料庫表的欄位命名規則是否有問題,但以這種方式作為引數列表的引數名,讓別人如何看懂?

在一種情況下我們應該懷疑一個方法是否放錯了類:絕大多數情況下,函式應該放在他所使用的資料的所屬Object(或者說class內),如果某個方法沒有使用任何所屬object的資訊,而是全部使用另一個object的資訊,我們應該考慮一下是否把這個方法移動一下

下一步,使用了Replace Temp with Query,即 將只是獲取某個返回值,但並未再作任何改變的臨時變數替換成了QueryMartin說這樣做的原因是他們會導致大量引數被傳來傳去,而其實完全沒有這個必要。你很容易失去他們的蹤跡,尤其在長長的函式之中更是如此。但這樣,我覺得勢必會以犧牲效能為代價,Martin也提到了效能的問題,他曾在第一章兩個地方談到“重構與效能”的問題。但他認為,重構時不必擔心效能,優化時才需要擔心他們,但那個時候你已處於一個比較有利的位置,有更多的選擇可以完成有效優化。但可能我還沒有在實際工作中考慮到或者說碰到“重構與效能”這個問題。所以這個問題有待進一步思考。

在這個簡單案例中,原本有一段邏輯是使用switch來根據一筆租片(Movie)的類別來計算它的費用。在這裡,它在另一個物件的屬性基礎上運用switch語句,不是一個好主意,如果不得不使用,也應該在物件自己的資料上使用,而不是在別人的資料上使用

重構技術提供了一種更高效且受控的程式碼整理技術。

    重構的目的是使軟體更容易被理解和修改。你可以在軟體內部做很多修改,但必須對軟體[可受觀察之外部行為]製造成很小的變化。或甚至不造成變化。與之形成對比的是[效能優化]。和重構一樣,效能優化通常不會改變元件的行為(除了執行速度),只會改變其內部結構。但是兩者出發點不同:效能優化往往使程式碼較難理解,但為了得到所需的效能你不得不那麼做。

    使用重構技術開發軟體時,你把自己的時間非賠給兩種截然不同的行為:新增新功能和重構。新增新功能時,你不應該修改既有程式碼,只管新增新功能。重構是你就不能再新增功能,只管改程式序的結構。你可能會在軟體開發過程中,這兩件事經常交替做,比如新增新功能時,發現程式結構改一下,功能新增會容易很多,於是進行重構,調整好後,又繼續之前新增新功能的工作。無論何時,你應該清楚自己到底在做哪件事。

    為何重構:

     改進軟體設計

     使軟體更易被理解

     助你找到Bug

     祝你提高程式設計速度

 

         Martin有一段文字對重複程式碼對優秀設計的影響作了討論,我覺得說得很透徹:

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

    一開始的重構可能只停留在細枝末節上。隨著程式碼漸趨簡潔,會發現自己可以看到一些以前看不到的設計層面的東西。如果不對程式碼作這些修改,也許我永遠看不見它們,因為我的聰明才智不足以在腦子裡把這一切都想象出來。

 

    何時重構:重構應該隨時隨地進行。你之所以重構,是因為你想做別的什麼事,而重構可以幫助你把那些事做好。

       三次法則:Three Strikes and you refactor

       新增功能時一併重構:使需要修改的程式碼更容易理解、程式碼的設計無法幫助我輕鬆新增我所需要的特性

       修補錯誤時一併重構:如果收到一份錯誤報告,這就是需要重構的訊號,因為顯然程式碼還不夠清晰——不夠清晰到讓你一目瞭然發現bug(但我覺得有了錯誤報告,就是重構的訊號有點過了)

       複審程式碼時一併重構:和某個團隊進行設計複審,而和一個複審者進行程式碼複審。極限程式設計中的成對程式設計形式,把程式碼複審的積極性發揮到了極致。一旦採用這種形式,所有正式的開發任務都由兩名開發者在同一臺機器上進行。這樣便在開發過程中形成隨時進行的程式碼複審工作,而重構也就被包含在開發過程內了

 

    什麼樣的程式難以修改:難以閱讀的、邏輯重複的、新增新行為時需要修改既有程式碼的、帶複雜條件邏輯的

       因此我們希望程式:容易閱讀、所有邏輯都只在唯一地點指定、新的改動不會危及現有行為、儘可能簡單表達條件邏輯

 

    間接層(Indirection)和重構:

       間接層的價值:

         允許邏輯共享:比如一個子函式在兩個不同的地點被呼叫,或superclass中的某個函式被所有subclasses共享

         分開解釋意圖和實現

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

         將條件邏輯加以編碼:物件有一種匪夷所思的機制——多型訊息(polymorphic messages),可以靈活彈性而清晰地表達條件邏輯。只要顯示條件邏輯被轉化為訊息(message)形式,往往便能降低程式碼的重複、增加清晰度並提高彈性(注:傳送訊息給某個物件,即呼叫某個函式)

 

    如果重構手法改變了已釋出介面,你必須同時維護新舊兩個介面,直到你的所有使用者都由這個時間對這個變化做出反應。這個時候應該讓舊介面呼叫新介面。千萬不要拷貝函式實現碼,這會讓你陷入重複程式碼的泥沼中難以自拔。你還應該是使用Java提供的deprecation

       Java之中還有一個特別關於修改介面的問題。在throws字句中增加一個異常,這並不是對

Signature的修改,所以你無法以delegation來隱藏它。由於這個原因,我總是喜歡為整個package定一個superclass異常(就象java.sqlSQLException),並確保所有public函式侄子自己的throws子句中宣告這個異常,這樣就可以解決上述的這個問題。

 

    在設計階段應該問問自己:把一個簡單的解決方案重構成這個靈活的方案有多大難度?如果答案是[相當容易],那麼你就只需要實現目前的簡單方案就行了

 

    當發現系統效能很差的時候:哪怕你完全瞭解你的程式,也請實際量測它的效能,不要臆測,臆測會讓你學到一些東西,但十有八九你是錯的

 

    重構與效能:重構往往會使軟體執行更慢,但它也使軟體的效能優化更易進行。除了對效能有嚴格要求的實時系統,其他任何情況下便攜快速軟體的祕密就是:首先寫出可調軟體,然後調整它以求獲得足夠速度

       三種編寫快速軟體的方法:

           時間預演算法:通常只用於效能要求極高的實時系統。如果使用這種方法,分解你的設計時就要做好預算,給每個元件預先分配一定的資源——包括時間和執行軌跡,每個元件都不能超過自己的預算

           持續關切法:這種方法要求任何程式設計師在任何時間做任何事時,都要設法保持系統的高效能。這種方法很常見,感覺上很有吸引力,但通常不會起太大作用。因為效能改善一旦被分散到程式各角落,每次改善都只不過是從對程式行為的一個狹隘視角出發而已。如果對大多數程式進行分析,你會發現它把大半時間都耗費在一小半程式碼上,90%的優化工作都是白費勁兒,因為被你優化的程式碼有許多很難被執行起來。

           利用上述90%統計資料:採用這種方法時,你以一種良好的分解方式來建造自己的程式,不對效能投以任何關注,直至進入效能優化階段——那通常在開發後期。一旦進入該階段,你再按照某個特定程式來調整程式效能。在效能優化階段,你首先應該以一個量測工具監控程式的執行,讓他告訴你程式中哪些地方大量消耗時間和空間。這樣你就可以找出效能熱點所在的一小段程式碼。然後你應該集中關切這些效能熱點,並使用前述持續關切法中的優化手段來優化它們。

相關文章