<script type="text/javascript"> google_ad_client = "pub-8800625213955058"; google_ad_slot = "0989131976"; google_ad_width = 336; google_ad_height = 280; // </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script> 這是關于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。 關于作者 石一楹,現任浙江大學靈峰科技開發公司技術總監。多年從事OO系統分析、設計。現主要研究方向為Refactoring和分析模式。你可以通過[email protected]和我聯系。
|