重構模式(一)---- 介紹

casularm發表於2004-10-23

        
        石一楹 (
shiyiying@hotmail.com)
        浙江大學靈峰科技開發公司技術總監
        2001 年 12 月
       
        這是關於refactoring思考的第一部分內容。本文將介紹refactoring的基本概念、定義,同時解釋正確、安全進行refactoring需要堅持的幾個原則。
             
        程式碼太容易變壞。程式碼總是趨向於有更大的類、更長的方法、更多的開關語句和更深的條件巢狀。重複程式碼隨處可見,特別是那些初看相似細看又不同的程式碼氾濫於整個系統:條件表示式,迴圈結構、集合列舉….資訊被共享於系統一些關係甚少的組成部分之間,通常,這使得系統中幾乎所有的重要資訊都變成全域性或者重複。你根本不能看到這種程式碼還有什麼良好的設計。(如果有的話,也已經不可辨識了。)
       
        這樣的程式碼難以理解,更不要說對它加以修改。如果你關心繫統體系結構、設計,或者是一個好程式,你的第一反應就是拒絕工作於這樣的程式碼。你會說:"這麼爛的程式碼,讓我修改,還不如重寫。"然而,你不大可能完全重寫已經能夠甚至是正在運作的系統,你不能保證新的系統能夠實現全部的原有功能。更何況,你不是生活在真空,還有更多的投資、交付、競爭壓力。
       
        於是你使用一種quick-and-dirty的方法,如果系統有問題,那麼就直接找到這個問題,便當地修改它。如果要增加一個新功能,你會從原來的系統中找到一塊相近的程式碼,拷出來,作些修改。對於原來的系統,你想,既然我不能重頭寫過,而且它們已經在運作,讓它去吧。然後,你增加的程式碼變成了下一個程式設計師咒罵的物件。系統越來越難以理解,維護越來越困難、越來越昂貴。系統變成了一個十足的大泥球。
       
        這種情況是每一個人都不願意碰到的,但是奇怪的是,這樣的情景一次又一次出現在大多數人的程式設計生涯中。這是因為我們不知道該如何解決。
       
        解決這個問題的最好辦法當然是讓它不要發生。然而,要阻止程式碼的腐化,你需要付出額外的代價。每次在修改或增加程式碼之前,你都要看一看手上的這些程式碼。如果它有很好的味道,那麼你應該能夠很方便地加入新的功能。如果你需要花很長的時間去理解原來的程式碼,花更長的時間去增加和修改程式碼。那麼,先放下手裡的活,讓我們來做Refactoring。
       
        什麼是Refactoring?
        
        每個人似乎都有自己的Refactoring的定義,儘管他們講的就是同一件事情。在那麼多的定義中,最先對Refactoring進行理論研究的Raloh Johnson的話顯然更有說服力:
             Refactoring是使用各種手段重新整理一個物件設計的過程,目的是為了讓設計更加靈活並且/或者更可重用。你可能有幾個理由來做這件事情,其中效率和可維護性可能是最重要的原因。
       
        Martin Fowler[Fowler]把Refactoring定義為兩部分,一部分為名詞形式:
             Refactoring(名詞): 在不改變可觀察行為的前提下,對軟體內部結構的改變,目的是使它更易於理解並且能夠更廉價地進行改變。
       
        另一部分則是動詞形式:
             Refactor(動詞): 通過應用一系列不改變軟體可觀察行為的refactoring來重構一個軟體。
        Martin Fowler的名詞形式就是說Refactoring是對軟體內部結構的改變,這種改變的前提是不能改變程式的可觀察的行為,這種改變的目的就是為了讓它更容易理解,更容易被修改。動詞形式則突出Refactor是一種軟體重構行為,這種重構的方法就是應用一系列的refactoring。
       
        軟體結構可以因為各種各樣的原因而被改變,如進行列印美化、效能優化等等,但只有出於可理解性、可修改、可維護目的的改變才是Refactoring。這種改變必須保持可觀察的行為,按照Martin的話來說,就是Refactoring之前軟體實現什麼功能,之後照樣實現什麼功能。任何使用者,不管是終端使用者還是其他的程式設計師,都不需要知道某些東西發生了變化。
       
        Refactoring原則
       
        Two Hats(兩頂帽子)
        Kent Beck提出這個比方。他說,如果你在使用Refactoring開發軟體,你把開發時間分給兩個不同的活動:增加功能和refactoring。增加功能時,你不應該改變任何已經存在的程式碼,你只是在增加新功能。這個時候,你增加新的測試,然後讓這些新測試能夠通過。當你換一頂帽子refactoring時,你要記住你不應該增加任何新功能,你只是在重構程式碼。你不會增加新的測試(除非發現以前漏掉了一個)。只有當你的Refactoring改變了一個原先程式碼的介面時才改變某些測試。
       
        在一個軟體的開發過程中,你可能頻繁地交換這兩頂帽子。你開始增加一個新功能,這時你認識到,如果原來的程式碼結構更好一點,新功能就能夠更方便地加入。因此,你脫下增加功能的帽子,換上refactoring的帽子。一會兒,程式碼結構變好了,你脫下refactoring的帽子,戴上增加功能的帽子。增加了新功能以後,你可能發現你的程式碼使得程式的結構難以理解,這時你又交換帽子。
       
        關於兩頂帽子交換的故事不斷地發生在你的日常開發中,但是不管你帶著哪一頂帽子,一定要記住帶一頂帽子只做一件事情。
       
        Unit Test
        保持程式碼的可觀察行為不變稱為Refactoring的安全性。Refactoring工具用半形式化的理論證明來保證Refactoring的安全性。
       
        但是,要從理論上完全證明系統的可觀察行為保持不變,雖然不是說不可能,也是十分困難的。工具也有自己的缺陷。首先,目前對於Refactoring的理論研究並非十分成熟,某些曾經被證明安全的Refactoring最近被發現在特定的場合下並不安全。其次,目前的工具不能很好地支援"非正式"的Refactoring操作,如果你發現一種新的Refactoring技巧,工具不能立即讓這種refactoring為你所用。
       
        自動化的測試是檢驗Refactoring安全性非常方便而且有效的方法。雖然我們不能窮盡整個系統中所有的測試,但如果在Refactoring之前成功的測試現在失敗了,我們就會知道剛剛做的Refactoring破壞了系統的可觀察行為。自動化測試能夠在程式設計師不進行人工干預的情況下自動檢測到這樣的行為破壞。
       
        自動化測試中最實用的工具是XUnit系列單元測試框架,該框架最初由Kent Beck和Eric Gamma為Smalltalk社團而開發。
       
        Eric Gamma對測試的重要性曾經有過這樣的話:
             你寫的測試越少,你的生產力就越低,同時你的程式碼就變得越不穩定。你越是沒有生產力、越缺少準確性,你承受的壓力就越大......
       
        下面的片斷來自Javaworld,兩個Sun開發者展示了它們對單元測試的狂熱以及展示了它們擴充套件單元測試來檢查象EJB這樣的分散式控制元件:
             我們從來沒有過度測試軟體,相反我們很少做得足夠。。。但願測試是軟體開發過程中關鍵但卻經常被誤解的一部分。對每一個程式碼單元而言,單元測試確保他自己能夠工作,獨立於其他單元。在面嚮物件語言中,一個單元通常,但並不總是,一個類的等價物。如果一個開發者確信應用程式的每一個片斷能夠按照它們被設計的方式正確工作,那麼他們會認識到組裝得到的應用程式發生的問題必定來自於把所有部件組合起來的過程中。單元測試告訴程式設計師一個應用程式' pieces are working as designed'。
       
        我曾經認為自己是很好的程式設計師。認為自己的程式碼幾乎不可能出錯。但事實上,我沒有任何證據可以證明這一點,同樣我也沒有信心我的程式碼就一定不會出錯,或者當我增加一項新功能時,原先的行為一定沒有遭到破壞。另一方面,我認為太多的測試於事無補,測試只能停留在理論之上,或只有那些實力強勁的大公司才能做到。
       
        這個觀點在1999年我看到Kent Beck和Gamma的Junit測試框架之後被完全推翻了。JUnit是XP的重要工具之一。XP提倡一個規則叫做test-first design。採用Test First Design方法,你在編寫一個新功能前先寫一個單元測試,用它來測試實現新功能需要但可能會出錯的程式碼。這意味著,測試首先是失敗的,寫程式碼的目的就是為了讓這些測試能夠成功執行。
       
        JUnit的簡單、易用和強大的功能幾乎讓我立刻接納了單元測試的思想,不但因為它可以讓我有證據表明我的程式碼是正確的,更重要的是在我每次對程式碼進行修改的同時,我有信心所有的變化都不會影響原有的功能。測試已經成為我所有程式碼的一部分。關於這一點,Kent Beck在它的《Extreme Programming Explained》中指出:
             簡直不存在一個不帶自動化測試的程式。程式設計師編寫單元測試,因而他們能夠確信程式操作的正確性成為程式本身的一部分。同時,客戶編寫功能測試,因而他們能夠確信程式操作的正確性成為程式本身的一部分。結果就是,隨著時間的推移,一個程式變得越來越可信-他變得更加能夠接受改變, 而不是相反。
       
        單元測試的基本過程如下:
       
        設計一個應當失敗的測試
        編譯器應當立刻反映出失敗。因為測試中需要使用的類和方法還沒有實現。
        如果有編譯錯誤,完成程式碼,只要讓編譯通過即可,這時的程式碼只反映了程式碼的意圖而並非實現。
        在JUnit中執行所有的測試,它應當指示測試失敗
        編寫實際程式碼,目的是為了讓測試能夠成功。
        在Junit中執行所有的測試,保證所有的測試全部通過,一旦所有的測試通過,停止編碼。
        考慮一下是否有其他情況沒有考慮到,編寫測試,執行它,必要時修改程式碼,直至測試通過
       
        在編寫測試的時候,要注意對測試的內容加以考慮,並不是測試越多越好.Kent Beck說:
             你不必為每一個方法編寫一個測試,只有那些可能出錯的具有生產力的方法才需要。有時你僅僅想找出某些事情是可能的。你探索一半個小時。是的,它有可能發生。現在你拋棄你的程式碼並且從單元測試重新開始。
       
        另一位作者Eric Gamma說:
             你總是能夠寫更多的測試。但是,你很快就會發現,你能夠想象到的測試中只有一部分才是真正有用的。你所需要的是為那些即使你認為它們應當工作還會出錯的地方編寫測試,或者是你認為可能會失敗但最終還是成功的地方。另一種方法是以成本/收益的角度來考慮。你應該編寫反饋資訊物有所值的測試。
       
        你可能會認為單元測試雖然好,但是它會增加你的程式設計負擔,而別人花錢是請你來寫程式碼,而不是來寫測試的。但是WILLAM WAKE說:
              編寫單元測試可能是一件乏味的事情,但是它們為你節省將來的時間(通過捕獲改變後的bug).相對不明顯,但同樣重要的是,他們能夠節約你現在的時間:測試聚焦於設計和實現的簡單性,它們支援refactoring,它們在你開發一項特性的同時對它進行驗證。
       
        你還會認為單元測試可能增加你的維護量,因為如果程式碼發生了改變,相應的測試也需要做出改變。事實上,測試只會讓你的維護更快,因為它們讓你對你所做出的改變更有信心,如果你做錯了一件事,測試同時也會提醒你。如果介面發生了改變,你當然需要改變你的介面,但這一點並非太難。
       
        單元測試是程式的一部分,而不是獨立的測試部門所應完成的任務。這就是所謂的自測試程式碼。程式設計師可能花費一些時間在編寫程式碼,花費一些時間在理解別人的程式碼,花費一些時間在做設計,但他們最多的時間是在做除錯。任何一個人都有這樣一種遭遇,一個小小的問題可能花費你一個下午、一天,甚至是幾天的時間來除錯。要改正一個bug往往很簡單,但是要找到這樣的bug卻是一個大問題。如果你的程式碼能夠帶有自動化的自測試,那麼一旦你加入一個新的功能,舊的測試會告訴你那些原來的程式碼存在著bug,而新加入的測試則告訴哪些新加入的程式碼引入了bug。
       
        Small step
        Refactoring的另一個原則就是每一步總是做很少的工作,每做少量修改,就進行測試,保證refactoring的程式是安全的。
       
        如果你一次做了太多的修改,那麼就有可能介入很多的bug,程式碼將難以除錯。如果你發現修改並不正確,要想返回到原來的狀態也十分困難。
       
        這些細小的步驟包括:
       
        尋找需要refactoring的地方。這些地方可能在理解程式碼、擴充套件系統的時候被發現。或者是通過聞程式碼的味道而找到。或者通過某些程式碼分析工具。
        如果對需要Refactoring的程式碼還沒有單元測試,首先編寫單元測試。如果已經有了單元測試,看一看該單元測試是否考慮到了你所面對的問題,不然就完善它。
        執行單元測試,保證原先的程式碼是正確的。
        根據程式碼所呈現的現象在頭腦中反映該做什麼樣的refactoring,或者找一本關於Refactoring分類目錄的書放在面前,查詢相似的情況,然後按照書中的指示一步一步進行Refactoring。
        每一步完成時,都進行單元測試,保證它的安全性,也就是可觀察行為沒有發生改變。
        如果Refactoring改變了介面,你需要修改測試套件
        全部做完後,進行全部的單元測試,功能測試,保證整個系統的可觀察行為不受影響。如果你按照這樣的步驟去做Refactoring,那麼可能出錯的機會就很小,正如Kent Beck所說:"I'm not a great programmer; I'm just a good programmer with great habits".
       
        要求使用小步驟漸進地Refactoring並不完全出於對實踐易行的考慮。
       
        Ralph Johnson在伊利諾斯州立大學領導的一個研究小組是Refactoring理論的引導者和最重要的理論研究團體。其中William Opdyke 1992年的博士論文《Refactoring Object-Oriented Framework》是公認的Refactoring第一位正式提出者。在那篇論文中,Opdyk描述他對refactoring重構層次的看法:
             通常,人們要麼在按照增加到系統的特性這樣一個高層次上,要麼按照被改變的程式碼行這樣一種低層次來看待軟體的變化。Refactorings是一種重組織計劃,它支援在一箇中間層次上的改變。例如,考慮一下,refactoring把一個類的成員函式移到另外一個類。。。。
       
        為了實現這樣的intermediate level操作,Opdyke提出了原子atomic refactoring的概念,他指出:
              下面列出的支援refactoring最終的分類是原子的;也就是說,它們是最原始級別的refactorings。原子refactoring建立、刪除、改變及移動實體…
        … 高層refactoring通過這26個低層(原子)refactoring得到支援
       
        論文中,Opdyke 首先證明在一定的前提之下,這些原子refactoring將不會改變程式的Observable behaviour。更高層的refactoring可以通過分解為這些原子的refactoring加以證明。Opdyke也證明了他所提出的高層refactoring如何在每一步原子atomic之後都符合後續原子atomic所需要的前提。
       
        小步前進使得對每一步進行證明成為可能,最終通過組合這些證明,可以從更高層次上來證明這些refactoring的安全性和正確性。
       
        Refactoring工具依賴於這些理論研究進行Refactoring。如果每個人能夠按照這樣的一小步一小步進行Refactoring,那麼極有希望他的refactoring能夠被正確地記錄下來,為整個物件導向社團所用。同時,對他理論正確性地證明可以促使refactoring工具得到進一步的發展。
       
        也許你會認為,隨著工具的發展,程式設計師將變成Refactoring機器人。這樣的看法是不正確的。
       
        雖然使用一個refactoring工具能夠避免介入使用手工方式可能產生的各種各樣bug,減少編譯、測試和code review。但是正如Smalltalk Refactory Browser的作者Don Roberts所說,Refactoring工具不打算用來代替程式設計師,程式設計師需要自己來決定什麼地方需要refactoring,做什麼樣的refactoring。而在這一點上,經驗是不可代替的。
       
        Code Review和Pair Programming
        要保證refactoring的正確性,還有一種很有用的方法就是進行Code Review。
       
        Code Review原先一般都在一些大公司實行,他們可能聘請專家對專案進行Code Review,以發現程式碼中存在的問題,改良系統的設計,提高程式設計師的水平。
       
        同樣在refactoring過程中,我們也可以使用Code Review的方法。問題是,我們是否有足夠的精力和人員配備來進行這樣的Review呢?
       
        XP成功經驗表明,Code Review不應當是只有大公司才能做的。甚之,XP中的Pair Programming其實就是對Code Review的極端化,它也更加適合於表達Code review在refactoring過程中所能起到的作用。Kent Beck說:
       
             在每一對中有兩個角色。一個合作者,把持鍵盤和滑鼠,正在考慮該處所實現方法的最佳途徑,。另一個合作者,則更多考慮策略性方面的問題:
       
        這是完成工作的所有過程嗎?
        還有沒有其他的測試套件還不能工作?
        還有沒有其他的方法可以簡化整個系統,從而使得當前的問題不再出現?
       
        使用這種方法進行refactoring,可以在一個程式設計師沒有想到一個應當有的單元測試時,當一個程式設計師無法找到合適的Refactoring方法或者當一個程式設計師沒有按照正確的方法進行refactoring時,另外一個程式設計師可以提出自己的觀點和建議。甚至在極端情況下,當擁有鍵盤的程式設計師對如何完成這個refactoring沒有概念時,另外一個程式設計師可以接過鍵盤,直接往下做。
       
        XPChina的notyy認為Code Review不應當屬於refactoring的原則之一。嚴格來你可以在不實行Pair Programming或者Code Review的情況下進行refactoring.但是由於refactoring的特殊性,它不是增加新的程式碼,而是修改已經存在、很可能已經被其他許多模組依賴的程式碼,所以Pair Programming在這裡比一般的新程式碼更重要。從另一個方面來講,如果你正在做big refactory,如refactor to Design pattern,此時Pair Programming更有助於交流雙方對於被修整程式碼將refactor成為何種設計模式的意見。
       
        因此,儘管這不是一條必要的原則,我還是把它作為原則之一進行描述。
       
        The Rule of Three
        Don Roberts提出的The Rule of Three好像和Pattern社團對模式的驗證十分相似:
       
        第一次做某件事,你直接做就是了。第二次你做某件事,看到重複,你有些退縮,但不管怎樣,你重複就是了。第三次你做類似的事情,你refactor。

 

相關文章