refactoring Patterns:第一部分

refactoring Patterns:第一部分

       這是關於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》中指出:
簡直不存在一個不帶自動化測試的程序。程序員編寫單元測試,因而他們能夠確信程序操作的正確性成爲程序本身的一部分。同時,客戶編寫功能測試,因而他們能夠確信程序操作的正確性成爲程序本身的一部分。結果就是,隨着時間的推移,一個程序變得越來越可信-他變得更加能夠接受改變,而不是相反。

單元測試的基本過程如下:

  1. 設計一個應當失敗的測試
  2. 編譯器應當立刻反映出失敗。因爲測試中需要使用的類和方法還沒有實現。
  3. 如果有編譯錯誤,完成代碼,只要讓編譯通過即可,這時的代碼只反映了代碼的意圖而並非實現。
  4. 在JUnit中運行所有的測試,它應當指示測試失敗
  5. 編寫實際代碼,目的是爲了讓測試能夠成功。
  6. 在Junit中運行所有的測試,保證所有的測試全部通過,一旦所有的測試通過,停止編碼。
  7. 考慮一下是否有其他情況沒有考慮到,編寫測試,運行它,必要時修改代碼,直至測試通過



 

在編寫測試的時候,要注意對測試的內容加以考慮,並不是測試越多越好.Kent Beck說:
你不必爲每一個方法編寫一個測試,只有那些可能出錯的具有生產力的方法才需要。有時你僅僅想找出某些事情是可能的。你探索一半個小時。是的,它有可能發生。現在你拋棄你的代碼並且從單元測試重新開始。

另一位作者Eric Gamma說:
你總是能夠寫更多的測試。但是,你很快就會發現,你能夠想象到的測試中只有一部分纔是真正有用的。你所需要的是爲那些即使你認爲它們應當工作還會出錯的地方編寫測試,或者是你認爲可能會失敗但最終還是成功的地方。另一種方法是以成本/收益的角度來考慮。你應該編寫反饋信息物有所值的測試。

你可能會認爲單元測試雖然好,但是它會增加你的編程負擔,而別人花錢是請你來寫代碼,而不是來寫測試的。但是WILLAM WAKE說:
編寫單元測試可能是一件乏味的事情,但是它們爲你節省將來的時間(通過捕獲改變後的bug).相對不明顯,但同樣重要的是,他們能夠節約你現在的時間:測試聚焦於設計和實現的簡單性,它們支持refactoring,它們在你開發一項特性的同時對它進行驗證。

你還會認爲單元測試可能增加你的維護量,因爲如果代碼發生了改變,相應的測試也需要做出改變。事實上,測試只會讓你的維護更快,因爲它們讓你對你所做出的改變更有信心,如果你做錯了一件事,測試同時也會提醒你。如果接口發生了改變,你當然需要改變你的接口,但這一點並非太難。

單元測試是程序的一部分,而不是獨立的測試部門所應完成的任務。這就是所謂的自測試代碼。程序員可能花費一些時間在編寫代碼,花費一些時間在理解別人的代碼,花費一些時間在做設計,但他們最多的時間是在做調試。任何一個人都有這樣一種遭遇,一個小小的問題可能花費你一個下午、一天,甚至是幾天的時間來調試。要改正一個bug往往很簡單,但是要找到這樣的bug卻是一個大問題。如果你的代碼能夠帶有自動化的自測試,那麼一旦你加入一個新的功能,舊的測試會告訴你那些原來的代碼存在着bug,而新加入的測試則告訴哪些新加入的代碼引入了bug。

Small step
Refactoring的另一個原則就是每一步總是做很少的工作,每做少量修改,就進行測試,保證refactoring的程序是安全的。

如果你一次做了太多的修改,那麼就有可能介入很多的bug,代碼將難以調試。如果你發現修改並不正確,要想返回到原來的狀態也十分困難。

這些細小的步驟包括:

  1. 尋找需要refactoring的地方。這些地方可能在理解代碼、擴展系統的時候被發現。或者是通過聞代碼的味道而找到。或者通過某些代碼分析工具。
  2. 如果對需要Refactoring的代碼還沒有單元測試,首先編寫單元測試。如果已經有了單元測試,看一看該單元測試是否考慮到了你所面對的問題,不然就完善它。
  3. 運行單元測試,保證原先的代碼是正確的。
  4. 根據代碼所呈現的現象在頭腦中反映該做什麼樣的refactoring,或者找一本關於Refactoring分類目錄的書放在面前,查找相似的情況,然後按照書中的指示一步一步進行Refactoring。
  5. 每一步完成時,都進行單元測試,保證它的安全性,也就是可觀察行爲沒有發生改變。
  6. 如果Refactoring改變了接口,你需要修改測試套件
  7. 全部做完後,進行全部的單元測試,功能測試,保證整個系統的可觀察行爲不受影響。如果你按照這樣的步驟去做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說:

在每一對中有兩個角色。一個合作者,把持鍵盤和鼠標,正在考慮該處所實現方法的最佳途徑,。另一個合作者,則更多考慮策略性方面的問題:

  1. 這是完成工作的所有過程嗎?
  2. 還有沒有其他的測試套件還不能工作?
  3. 還有沒有其他的方法可以簡化整個系統,從而使得當前的問題不再出現?



 

使用這種方法進行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。

發佈了27 篇原創文章 · 獲贊 3 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章