重構(gòu)的原則(關(guān)于重構(gòu)原則)
譯者:熊節(jié), 林從羽
前一章所舉的例子應(yīng)該已經(jīng)讓你對(duì)重構(gòu)有了一個(gè)良好的感覺。現(xiàn)在,我們應(yīng)該回頭看看重構(gòu)的一些大原則。
##2.1 何謂重構(gòu)
一線的實(shí)踐者們經(jīng)常很隨意地使用“重構(gòu)”這個(gè)詞——軟件開發(fā)領(lǐng)域的很多詞匯都有此待遇。我使用這個(gè)詞的方式比較嚴(yán)謹(jǐn),并且我發(fā)現(xiàn)這種嚴(yán)謹(jǐn)?shù)姆绞胶苡泻锰帯#ㄏ铝卸x與本書第1版中給出的定義一樣。)“重構(gòu)”這個(gè)詞既可以用作名詞也可以用作動(dòng)詞。名詞形式的定義是:
重構(gòu)(名詞):對(duì)軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
這個(gè)定義適用于我在前面的例子中提到的那些有名字的重構(gòu),例如提煉函數(shù)(106)和以多態(tài)取代條件表達(dá)式(272)。
動(dòng)詞形式的定義是:
重構(gòu)(動(dòng)詞):使用一系列重構(gòu)手法,在不改變軟件可觀察行為的前提下,調(diào)整其結(jié)構(gòu)。
所以,我可能會(huì)花一兩個(gè)小時(shí)進(jìn)行重構(gòu)(動(dòng)詞),其間我會(huì)使用幾十個(gè)不同的重構(gòu)(名詞)。
過去十幾年,這個(gè)行業(yè)里的很多人用“重構(gòu)”這個(gè)詞來指代任何形式的代碼清理,但上面的定義所指的是一種特定的清理代碼的方式。重構(gòu)的關(guān)鍵在于運(yùn)用大量微小且保持軟件行為的步驟,一步步達(dá)成大規(guī)模的修改。每個(gè)單獨(dú)的重構(gòu)要么很小,要么由若干小步驟組合而成。因此,在重構(gòu)的過程中,我的代碼很少進(jìn)入不可工作的狀態(tài),即便重構(gòu)沒有完成,我也可以在任何時(shí)刻停下來。
如果有人說他們的代碼在重構(gòu)過程中有一兩天時(shí)間不可用,基本上可以確定,他們?cè)谧龅氖虏皇侵貥?gòu)。
我會(huì)用“結(jié)構(gòu)調(diào)整”(restructuring)來泛指對(duì)代碼庫(kù)進(jìn)行的各種形式的重新組織或清理,重構(gòu)則是特定的一類結(jié)構(gòu)調(diào)整。剛接觸重構(gòu)的人看我用很多小步驟完成似乎可以一大步就能做完的事,可能會(huì)覺得這樣很低效。但小步前進(jìn)能讓我走得更快,因?yàn)檫@些小步驟能完美地彼此組合,而且——更關(guān)鍵的是——整個(gè)過程中我不會(huì)花任何時(shí)間來調(diào)試。
在上述定義中,我用了“可觀察行為”的說法。它的意思是,整體而言,經(jīng)過重構(gòu)之后的代碼所做的事應(yīng)該與重構(gòu)之前大致一樣。這個(gè)說法并非完全嚴(yán)格,并且我是故意保留這點(diǎn)兒空間的:重構(gòu)之后的代碼不一定與重構(gòu)前行為完全一致。比如說,提煉函數(shù)(106)會(huì)改變函數(shù)調(diào)用棧,因此程序的性能就會(huì)有所改變;改變函數(shù)聲明(124)和搬移函數(shù)(198)等重構(gòu)經(jīng)常會(huì)改變模塊的接口。不過就用戶應(yīng)該關(guān)心的行為而言,不應(yīng)該有任何改變。如果我在重構(gòu)過程中發(fā)現(xiàn)了任何bug,重構(gòu)完成后同樣的bug應(yīng)該仍然存在(不過,如果潛在的bug還沒有被任何人發(fā)現(xiàn),也可以當(dāng)即把它改掉)。
重構(gòu)與性能優(yōu)化有很多相似之處:兩者都需要修改代碼,并且兩者都不會(huì)改變程序的整體功能。兩者的差別在于其目的:重構(gòu)是為了讓代碼“更容易理解,更易于修改”。這可能使程序運(yùn)行得更快,也可能使程序運(yùn)行得更慢。在性能優(yōu)化時(shí),我只關(guān)心讓程序運(yùn)行得更快,最終得到的代碼有可能更難理解和維護(hù),對(duì)此我有心理準(zhǔn)備。
##2.2 兩頂帽子
Kent Beck提出了“兩頂帽子”的比喻。使用重構(gòu)技術(shù)開發(fā)軟件時(shí),我把自己的時(shí)間分配給兩種截然不同的行為:添加新功能和重構(gòu)。添加新功能時(shí),我不應(yīng)該修改既有代碼,只管添加新功能。通過添加測(cè)試并讓測(cè)試正常運(yùn)行,我可以衡量自己的工作進(jìn)度。重構(gòu)時(shí)我就不能再添加功能,只管調(diào)整代碼的結(jié)構(gòu)。此時(shí)我不應(yīng)該添加任何測(cè)試(除非發(fā)現(xiàn)有先前遺漏的東西),只在絕對(duì)必要(用以處理接口變化)時(shí)才修改測(cè)試。
軟件開發(fā)過程中,我可能會(huì)發(fā)現(xiàn)自己經(jīng)常變換帽子。首先我會(huì)嘗試添加新功能,然后會(huì)意識(shí)到:如果把程序結(jié)構(gòu)改一下,功能的添加會(huì)容易得多。于是我換一頂帽子,做一會(huì)兒重構(gòu)工作。程序結(jié)構(gòu)調(diào)整好后,我又換上原先的帽子,繼續(xù)添加新功能。新功能正常工作后,我又發(fā)現(xiàn)自己的編碼造成程序難以理解,于是又換上重構(gòu)帽子……整個(gè)過程或許只花10分鐘,但無論何時(shí)我都清楚自己戴的是哪一頂帽子,并且明白不同的帽子對(duì)編程狀態(tài)提出的不同要求。
##2.3 為何重構(gòu)
我不想把重構(gòu)說成是包治百病的萬靈丹,它絕對(duì)不是所謂的“銀彈”。不過它的確很有價(jià)值,盡管它不是一顆“銀彈”,卻可以算是一把“銀鉗子”,可以幫你始終良好地控制自己的代碼。重構(gòu)是一個(gè)工具,它可以(并且應(yīng)該)用于以下幾個(gè)目的。
###重構(gòu)改進(jìn)軟件的設(shè)計(jì)
如果沒有重構(gòu),程序的內(nèi)部設(shè)計(jì)(或者叫架構(gòu))會(huì)逐漸腐敗變質(zhì)。當(dāng)人們只為短期目的而修改代碼時(shí),他們經(jīng)常沒有完全理解架構(gòu)的整體設(shè)計(jì),于是代碼逐漸失去了自己的結(jié)構(gòu)。程序員越來越難通過閱讀源碼來理解原來的設(shè)計(jì)。代碼結(jié)構(gòu)的流失有累積效應(yīng)。越難看出代碼所代表的設(shè)計(jì)意圖,就越難保護(hù)其設(shè)計(jì),于是設(shè)計(jì)就腐敗得越快。經(jīng)常性的重構(gòu)有助于代碼維持自己該有的形態(tài)。
完成同樣一件事,設(shè)計(jì)欠佳的程序往往需要更多代碼,這常常是因?yàn)榇a在不同的地方使用完全相同的語(yǔ)句做同樣的事,因此改進(jìn)設(shè)計(jì)的一個(gè)重要方向就是消除重復(fù)代碼。代碼量減少并不會(huì)使系統(tǒng)運(yùn)行更快,因?yàn)檫@對(duì)程序的資源占用幾乎沒有任何明顯影響。然而代碼量減少將使未來可能的程序修改動(dòng)作容易得多。代碼越多,做正確的修改就越困難,因?yàn)橛懈啻a需要理解。我在這里做了點(diǎn)兒修改,系統(tǒng)卻不如預(yù)期那樣工作,因?yàn)槲覜]有修改另一處——那里的代碼做著幾乎完全一樣的事情,只是所處環(huán)境略有不同。消除重復(fù)代碼,我就可以確定所有事物和行為在代碼中只表述一次,這正是優(yōu)秀設(shè)計(jì)的根本。
###重構(gòu)使軟件更容易理解
所謂程序設(shè)計(jì),很大程度上就是與計(jì)算機(jī)對(duì)話:我編寫代碼告訴計(jì)算機(jī)做什么事,而它的響應(yīng)是按照我的指示精確行動(dòng)。一言以蔽之,我所做的就是填補(bǔ)“我想要它做什么”和“我告訴它做什么”之間的縫隙。編程的核心就在于“準(zhǔn)確說出我想要的”。然而別忘了,除了計(jì)算機(jī)外,源碼還有其他讀者:幾個(gè)月之后可能會(huì)有另一位程序員嘗試讀懂我的代碼并對(duì)其做一些修改。我們很容易忘記這這位讀者,但他才是最重要的。計(jì)算機(jī)是否多花了幾個(gè)時(shí)鐘周期來編譯,又有什么關(guān)系呢?如果一個(gè)程序員花費(fèi)一周時(shí)間來修改某段代碼,那才要命呢——如果他理解了我的代碼,這個(gè)修改原本只需一小時(shí)。
問題在于,當(dāng)我努力讓程序運(yùn)轉(zhuǎn)的時(shí)候,我不會(huì)想到未來出現(xiàn)的那個(gè)開發(fā)者。是的,我們應(yīng)該改變一下開發(fā)節(jié)奏,讓代碼變得更易于理解。重構(gòu)可以幫我讓代碼更易讀。開始進(jìn)行重構(gòu)前,代碼可以正常運(yùn)行,但結(jié)構(gòu)不夠理想。在重構(gòu)上花一點(diǎn)點(diǎn)時(shí)間,就可以讓代碼更好地表達(dá)自己的意圖——更清晰地說出我想要做的。
關(guān)于這一點(diǎn),我沒必要表現(xiàn)得多么無私。很多時(shí)候那個(gè)未來的開發(fā)者就是我自己。此時(shí)重構(gòu)就顯得尤其重要了。我是一個(gè)很懶惰的程序員,我的懶惰表現(xiàn)形式之一就是:總是記不住自己寫過的代碼。事實(shí)上,對(duì)于任何能夠立刻查閱的東西,我都故意不去記它,因?yàn)槲遗掳炎约旱哪X袋塞爆。我總是盡量把該記住的東西寫進(jìn)代碼里,這樣我就不必記住它了。這么一來,下班后我還可以喝上兩杯Maudite啤酒,不必太擔(dān)心它殺光我的腦細(xì)胞。
###重構(gòu)幫助找到bug
對(duì)代碼的理解,可以幫我找到bug。我承認(rèn)我不太擅長(zhǎng)找bug。有些人只要盯著一大段代碼就可以找出里面的bug,我不行。但我發(fā)現(xiàn),如果對(duì)代碼進(jìn)行重構(gòu),我就可以深入理解代碼的所作所為,并立即把新的理解反映在代碼當(dāng)中。搞清楚程序結(jié)構(gòu)的同時(shí),我也驗(yàn)證了自己所做的一些假設(shè),于是想不把bug揪出來都難。
這讓我想起了Kent Beck經(jīng)常形容自己的一句話:“我不是一個(gè)特別好的程序員,我只是一個(gè)有著一些特別好的習(xí)慣的還不錯(cuò)的程序員。”重構(gòu)能夠幫助我更有效地寫出健壯的代碼。
###重構(gòu)提高編程速度
最后,前面的一切都?xì)w結(jié)到了這一點(diǎn):重構(gòu)幫我更快速地開發(fā)程序。
聽起來有點(diǎn)兒違反直覺。當(dāng)我談到重構(gòu)時(shí),人們很容易看出它能夠提高質(zhì)量。改善設(shè)計(jì)、提升可讀性、減少bug,這些都能提高質(zhì)量。但花在重構(gòu)上的時(shí)間,難道不是在降低開發(fā)速度嗎?
當(dāng)我跟那些在一個(gè)系統(tǒng)上工作較長(zhǎng)時(shí)間的軟件開發(fā)者交談時(shí),經(jīng)常會(huì)聽到這樣的故事:一開始他們進(jìn)展很快,但如今想要添加一個(gè)新功能需要的時(shí)間就要長(zhǎng)得多。他們需要花越來越多的時(shí)間去考慮如何把新功能塞進(jìn)現(xiàn)有的代碼庫(kù),不斷蹦出來的bug修復(fù)起來也越來越慢。代碼庫(kù)看起來就像補(bǔ)丁摞補(bǔ)丁,需要細(xì)致的考古工作才能弄明白整個(gè)系統(tǒng)是如何工作的。這份負(fù)擔(dān)不斷拖慢新增功能的速度,到最后程序員恨不得從頭開始重寫整個(gè)系統(tǒng)。
下面這幅圖可以描繪他們經(jīng)歷的困境。
但有些團(tuán)隊(duì)的境遇則截然不同。他們添加新功能的速度越來越快,因?yàn)樗麄兡芾靡延械墓δ埽谝延械墓δ芸焖贅?gòu)建新功能。
兩種團(tuán)隊(duì)的區(qū)別就在于軟件的內(nèi)部質(zhì)量。需要添加新功能時(shí),內(nèi)部質(zhì)量良好的軟件讓我可以很容易找到在哪里修改、如何修改。良好的模塊劃分使我只需要理解代碼庫(kù)的一小部分,就可以做出修改。如果代碼很清晰,我引入bug的可能性就會(huì)變小,即使引入了bug,調(diào)試也會(huì)容易得多。理想情況下,我的代碼庫(kù)會(huì)逐步演化成一個(gè)平臺(tái),在其上可以很容易地構(gòu)造與其領(lǐng)域相關(guān)的新功能。
我把這種現(xiàn)象稱為“設(shè)計(jì)耐久性假說”:通過投入精力改善內(nèi)部設(shè)計(jì),我們?cè)黾恿塑浖哪途眯裕瑥亩梢愿L(zhǎng)時(shí)間地保持開發(fā)的快速。我還無法科學(xué)地證明這個(gè)理論,所以我說它是一個(gè)“假說”。但我的經(jīng)驗(yàn),以及我在職業(yè)生涯中認(rèn)識(shí)的上百名優(yōu)秀程序員的經(jīng)驗(yàn),都支持這個(gè)假說。
20年前,行業(yè)的陳規(guī)認(rèn)為:良好的設(shè)計(jì)必須在開始編程之前完成,因?yàn)橐坏╅_始編寫代碼,設(shè)計(jì)就只會(huì)逐漸腐敗。重構(gòu)改變了這個(gè)圖景。現(xiàn)在我們可以改善已有代碼的設(shè)計(jì),因此我們可以先做一個(gè)設(shè)計(jì),然后不斷改善它,哪怕程序本身的功能也在不斷發(fā)生著變化。由于預(yù)先做出良好的設(shè)計(jì)非常困難,想要既體面又快速地開發(fā)功能,重構(gòu)必不可少。
##2.4 何時(shí)重構(gòu)
在我編程的每個(gè)小時(shí),我都會(huì)做重構(gòu)。有幾種方式可以把重構(gòu)融入我的工作過程里。
{三次法則}
Don Roberts給了我一條準(zhǔn)則:第一次做某件事時(shí)只管去做;第二次做類似的事會(huì)產(chǎn)生反感,但無論如何還是可以去做;第三次再做類似的事,你就應(yīng)該重構(gòu)。
正如老話說的:事不過三,三則重構(gòu)。
###預(yù)備性重構(gòu):讓添加新功能更容易
重構(gòu)的最佳時(shí)機(jī)就在添加新功能之前。在動(dòng)手添加新功能之前,我會(huì)看看現(xiàn)有的代碼庫(kù),此時(shí)經(jīng)常會(huì)發(fā)現(xiàn):如果對(duì)代碼結(jié)構(gòu)做一點(diǎn)微調(diào),我的工作會(huì)容易得多。也許已經(jīng)有個(gè)函數(shù)提供了我需要的大部分功能,但有幾個(gè)字面量的值與我的需要略有沖突。如果不做重構(gòu),我可能會(huì)把整個(gè)函數(shù)復(fù)制過來,修改這幾個(gè)值,但這就會(huì)導(dǎo)致重復(fù)代碼——如果將來我需要做修改,就必須同時(shí)修改兩處(更麻煩的是,我得先找到這兩處)。而且,如果將來我還需要一個(gè)類似又略有不同的功能,就只能再?gòu)?fù)制粘貼一次,這可不是個(gè)好主意。所以我戴上重構(gòu)的帽子,使用函數(shù)參數(shù)化(310)。做完這件事以后,接下來我就只需要調(diào)用這個(gè)函數(shù),傳入我需要的參數(shù)。
這就好像我要往東去100公里。我不會(huì)往東一頭把車開進(jìn)樹林,而是先往北開20公里上高速,然后再向東開100公里。后者的速度比前者要快上3倍。如果有人催著你“趕快直接去那兒”,有時(shí)你需要說:“等等,我要先看看地圖,找出最快的路徑。”這就是預(yù)備性重構(gòu)于我的意義。
{--:}——Jessica Kerr
修復(fù)bug時(shí)的情況也是一樣。在尋找問題根因時(shí),我可能會(huì)發(fā)現(xiàn):如果把3段一模一樣且都會(huì)導(dǎo)致錯(cuò)誤的代碼合并到一處,問題修復(fù)起來會(huì)容易得多。或者,如果把某些更新數(shù)據(jù)的邏輯與查詢邏輯分開,會(huì)更容易避免造成錯(cuò)誤的邏輯糾纏。用重構(gòu)改善這些情況,在同樣場(chǎng)合再次出現(xiàn)同樣bug的概率也會(huì)降低。
###幫助理解的重構(gòu):使代碼更易懂
我需要先理解代碼在做什么,然后才能著手修改。這段代碼可能是我寫的,也可能是別人寫的。一旦我需要思考“這段代碼到底在做什么”,我就會(huì)自問:能不能重構(gòu)這段代碼,令其一目了然?我可能看見了一段結(jié)構(gòu)糟糕的條件邏輯,也可能希望復(fù)用一個(gè)函數(shù),但花費(fèi)了幾分鐘才弄懂它到底在做什么,因?yàn)樗暮瘮?shù)命名實(shí)在是太糟糕了。這些都是重構(gòu)的機(jī)會(huì)。
看代碼時(shí),我會(huì)在腦海里形成一些理解,但我的記性不好,記不住那么多細(xì)節(jié)。正如Ward Cunningham所說,通過重構(gòu),我就把腦子里的理解轉(zhuǎn)移到了代碼本身。隨后我運(yùn)行這個(gè)軟件,看它是否正常工作,來檢查這些理解是否正確。如果把對(duì)代碼的理解植入代碼中,這份知識(shí)會(huì)保存得更久,并且我的同事也能看到。
重構(gòu)帶來的幫助不僅發(fā)生在將來——常常是立竿見影。我會(huì)先在一些小細(xì)節(jié)上使用重構(gòu)來幫助理解,給一兩個(gè)變量改名,讓它們更清楚地表達(dá)意圖,以方便理解,或是將一個(gè)長(zhǎng)函數(shù)拆成幾個(gè)小函數(shù)。當(dāng)代碼變得更清晰一些時(shí),我就會(huì)看見之前看不見的設(shè)計(jì)問題。如果不做前面的重構(gòu),我可能永遠(yuǎn)都看不見這些設(shè)計(jì)問題,因?yàn)槲也粔蚵斆鳎瑹o法在腦海中推演所有這些變化。Ralph Johnson說,這些初步的重構(gòu)就像掃去窗上的塵埃,使我們得以看到窗外的風(fēng)景。在研讀代碼時(shí),重構(gòu)會(huì)引領(lǐng)我獲得更高層面的理解,如果只是閱讀代碼很難有此領(lǐng)悟。有些人以為這些重構(gòu)只是毫無意義地把玩代碼,他們沒有意識(shí)到,缺少了這些細(xì)微的整理,他們就無法看到隱藏在一片混亂背后的機(jī)遇。
###撿垃圾式重構(gòu)
幫助理解的重構(gòu)還有一個(gè)變體:我已經(jīng)理解代碼在做什么,但發(fā)現(xiàn)它做得不好,例如邏輯不必要地迂回復(fù)雜,或者兩個(gè)函數(shù)幾乎完全相同,可以用一個(gè)參數(shù)化的函數(shù)取而代之。這里有一個(gè)取舍:我不想從眼下正要完成的任務(wù)上跑題太多,但我也不想把垃圾留在原地,給將來的修改增加麻煩。如果我發(fā)現(xiàn)的垃圾很容易重構(gòu),我會(huì)馬上重構(gòu)它;如果重構(gòu)需要花一些精力,我可能會(huì)拿一張便箋紙把它記下來,完成當(dāng)下的任務(wù)再回來重構(gòu)它。
當(dāng)然,有時(shí)這樣的垃圾需要好幾個(gè)小時(shí)才能解決,而我又有更緊急的事要完成。不過即便如此,稍微花一點(diǎn)工夫做一點(diǎn)兒清理,通常都是值得的。正如野營(yíng)者的老話所說:至少要讓營(yíng)地比你到達(dá)時(shí)更干凈。如果每次經(jīng)過這段代碼時(shí)都把它變好一點(diǎn)點(diǎn),積少成多,垃圾總會(huì)被處理干凈。重構(gòu)的妙處就在于,每個(gè)小步驟都不會(huì)破壞代碼——所以,有時(shí)一塊垃圾在好幾個(gè)月之后才終于清理干凈,但即便每次清理并不完整,代碼也不會(huì)被破壞。
###有計(jì)劃的重構(gòu)和見機(jī)行事的重構(gòu)
上面的例子——預(yù)備性重構(gòu)、幫助理解的重構(gòu)、撿垃圾式重構(gòu)——都是見機(jī)行事的:我并不專門安排一段時(shí)間來重構(gòu),而是在添加功能或修復(fù)bug的同時(shí)順便重構(gòu)。這是我自然的編程流的一部分。不管是要添加功能還是修復(fù)bug,重構(gòu)對(duì)我當(dāng)下的任務(wù)有幫助,而且讓我未來的工作更輕松。這是一件很重要而又常被誤解的事:重構(gòu)不是與編程割裂的行為。你不會(huì)專門安排時(shí)間重構(gòu),正如你不會(huì)專門安排時(shí)間寫if語(yǔ)句。我的項(xiàng)目計(jì)劃上沒有專門留給重構(gòu)的時(shí)間,絕大多數(shù)重構(gòu)都在我做其他事的過程中自然發(fā)生。
骯臟的代碼必須重構(gòu),但漂亮的代碼也需要很多重構(gòu)。
還有一種常見的誤解認(rèn)為,重構(gòu)就是人們彌補(bǔ)過去的錯(cuò)誤或者清理骯臟的代碼。當(dāng)然,如果遇上了骯臟的代碼,你必須重構(gòu),但漂亮的代碼也需要很多重構(gòu)。在寫代碼時(shí),我會(huì)做出很多權(quán)衡取舍:參數(shù)化需要做到什么程度?函數(shù)之間的邊界應(yīng)該劃在哪里?對(duì)于昨天的功能完全合理的權(quán)衡,在今天要添加新功能時(shí)可能就不再合理。好在,當(dāng)我需要改變這些權(quán)衡以反映現(xiàn)實(shí)情況的變化時(shí),整潔的代碼重構(gòu)起來會(huì)更容易。
每次要修改時(shí),首先令修改很容易(警告:這件事有時(shí)會(huì)很難),然后再進(jìn)行這次容易的修改。
{--:}——Kent Beck
長(zhǎng)久以來,人們認(rèn)為編寫軟件是一個(gè)累加的過程:要添加新功能,我們就應(yīng)該增加新代碼。但優(yōu)秀的程序員知道,添加新功能最快的方法往往是先修改現(xiàn)有的代碼,使新功能容易被加入。所以,軟件永遠(yuǎn)不應(yīng)該被視為“完成”。每當(dāng)需要新能力時(shí),軟件就應(yīng)該做出相應(yīng)的改變。越是在已有代碼中,這樣的改變就越顯重要。
不過,說了這么多,并不表示有計(jì)劃的重構(gòu)總是錯(cuò)的。如果團(tuán)隊(duì)過去忽視了重構(gòu),那么常常會(huì)需要專門花一些時(shí)間來優(yōu)化代碼庫(kù),以便更容易添加新功能。在重構(gòu)上花一個(gè)星期的時(shí)間,會(huì)在未來幾個(gè)月里發(fā)揮價(jià)值。有時(shí),即便團(tuán)隊(duì)做了日常的重構(gòu),還是會(huì)有問題在某個(gè)區(qū)域逐漸累積長(zhǎng)大,最終需要專門花些時(shí)間來解決。但這種有計(jì)劃的重構(gòu)應(yīng)該很少,大部分重構(gòu)應(yīng)該是不起眼的、見機(jī)行事的。
我聽過的一條建議是:將重構(gòu)與添加新功能在版本控制的提交中分開。這樣做的一大好處是可以各自獨(dú)立地審閱和批準(zhǔn)這些提交。但我并不認(rèn)同這種做法。重構(gòu)常常與新添功能緊密交織,不值得花工夫把它們分開。并且這樣做也使重構(gòu)脫離了上下文,使人看不出這些“重構(gòu)提交”的價(jià)值。每個(gè)團(tuán)隊(duì)?wèi)?yīng)該嘗試并找出適合自己的工作方式,只是要記住:分離重構(gòu)提交并不是毋庸置疑的原則,只有當(dāng)你真的感到有益時(shí),才值得這樣做。
###長(zhǎng)期重構(gòu)
大多數(shù)重構(gòu)可以在幾分鐘——最多幾小時(shí)——內(nèi)完成。但有一些大型的重構(gòu)可能要花上幾個(gè)星期,例如要替換一個(gè)正在使用的庫(kù),或者將整塊代碼抽取到一個(gè)組件中并共享給另一支團(tuán)隊(duì)使用,再或者要處理一大堆混亂的依賴關(guān)系,等等。
即便在這樣的情況下,我仍然不愿讓一支團(tuán)隊(duì)專門做重構(gòu)。可以讓整個(gè)團(tuán)隊(duì)達(dá)成共識(shí),在未來幾周時(shí)間里逐步解決這個(gè)問題,這經(jīng)常是一個(gè)有效的策略。每當(dāng)有人靠近“重構(gòu)區(qū)”的代碼,就把它朝想要改進(jìn)的方向推動(dòng)一點(diǎn)。這個(gè)策略的好處在于,重構(gòu)不會(huì)破壞代碼——每次小改動(dòng)之后,整個(gè)系統(tǒng)仍然照常工作。例如,如果想替換掉一個(gè)正在使用的庫(kù),可以先引入一層新的抽象,使其兼容新舊兩個(gè)庫(kù)的接口。一旦調(diào)用方已經(jīng)完全改為使用這層抽象,替換下面的庫(kù)就會(huì)容易得多。(這個(gè)策略叫作Branch By Abstraction[mf-bba]。)
###復(fù)審代碼時(shí)重構(gòu)
一些公司會(huì)做常規(guī)的代碼復(fù)審(code review),因?yàn)檫@種活動(dòng)可以改善開發(fā)狀況。代碼復(fù)審有助于在開發(fā)團(tuán)隊(duì)中傳播知識(shí),也有助于讓較有經(jīng)驗(yàn)的開發(fā)者把知識(shí)傳遞給比較欠缺經(jīng)驗(yàn)的人,并幫助更多人理解大型軟件系統(tǒng)中的更多部分。代碼復(fù)審對(duì)于編寫清晰代碼也很重要。我的代碼也許對(duì)我自己來說很清晰,對(duì)他人則不然。這是無法避免的,因?yàn)橐岄_發(fā)者設(shè)身處地為那些不熟悉自己所作所為的人著想,實(shí)在太困難了。代碼復(fù)審也讓更多人有機(jī)會(huì)提出有用的建議,畢竟我在一個(gè)星期之內(nèi)能夠想出的好點(diǎn)子很有限。如果能得到別人的幫助,我的生活會(huì)滋潤(rùn)得多,所以我總是期待更多復(fù)審。
我發(fā)現(xiàn),重構(gòu)可以幫助我復(fù)審別人的代碼。開始重構(gòu)前我可以先閱讀代碼,得到一定程度的理解,并提出一些建議。一旦想到一些點(diǎn)子,我就會(huì)考慮是否可以通過重構(gòu)立即輕松地實(shí)現(xiàn)它們。如果可以,我就會(huì)動(dòng)手。這樣做了幾次以后,我可以更清楚地看到,當(dāng)我的建議被實(shí)施以后,代碼會(huì)是什么樣。我不必想象代碼應(yīng)該是什么樣,我可以真實(shí)看見。于是我可以獲得更高層次的認(rèn)識(shí)。如果不進(jìn)行重構(gòu),我永遠(yuǎn)無法得到這樣的認(rèn)識(shí)。
重構(gòu)還可以幫助代碼復(fù)審工作得到更具體的結(jié)果。不僅獲得建議,而且其中許多建議能夠立刻實(shí)現(xiàn)。最終你將從實(shí)踐中得到比以往多得多的成就感。
###怎么對(duì)經(jīng)理說
“該怎么跟經(jīng)理說重構(gòu)的事?”這是我最常被問到的一個(gè)問題。毋庸諱言,我見過一些場(chǎng)合,“重構(gòu)”被視為一個(gè)臟詞——經(jīng)理(和客戶)認(rèn)為重構(gòu)要么是在彌補(bǔ)過去犯下的錯(cuò)誤,要么是不增加價(jià)值的無用功。如果團(tuán)隊(duì)又計(jì)劃了幾周時(shí)間專門做重構(gòu),情況就更糟糕了——如果他們做的其實(shí)還不是重構(gòu),而是不加小心的結(jié)構(gòu)調(diào)整,然后又對(duì)代碼庫(kù)造成了破壞,那可就真是糟透了。
如果這位經(jīng)理懂技術(shù),能理解“設(shè)計(jì)耐久性假說”,那么向他說明重構(gòu)的意義應(yīng)該不會(huì)很困難。這樣的經(jīng)理應(yīng)該會(huì)鼓勵(lì)日常的重構(gòu),并主動(dòng)尋找團(tuán)隊(duì)日常重構(gòu)做得不夠的征兆。雖然“團(tuán)隊(duì)做了太多重構(gòu)”的情況確實(shí)也發(fā)生過,但比起做得不夠的情況要罕見得多了。
當(dāng)然,很多經(jīng)理和客戶不具備這樣的技術(shù)意識(shí),他們不理解代碼庫(kù)的健康對(duì)生產(chǎn)率的影響。這種情況下我會(huì)給團(tuán)隊(duì)一個(gè)較有爭(zhēng)議的建議:不要告訴經(jīng)理!
這是在搞破壞嗎?我不這樣想。軟件開發(fā)者都是專業(yè)人士。我們的工作就是盡可能快速創(chuàng)造出高效軟件。我的經(jīng)驗(yàn)告訴我,對(duì)于快速創(chuàng)造軟件,重構(gòu)可帶來巨大幫助。如果需要添加新功能,而原本設(shè)計(jì)卻又使我無法方便地修改,我發(fā)現(xiàn)先重構(gòu)再添加新功能會(huì)更快些。如果要修補(bǔ)錯(cuò)誤,就得先理解軟件的工作方式,而我發(fā)現(xiàn)重構(gòu)是理解軟件的最快方式。受進(jìn)度驅(qū)動(dòng)的經(jīng)理要我盡可能快速完成任務(wù),至于怎么完成,那就是我的事了。我領(lǐng)這份工資,是因?yàn)槲疑瞄L(zhǎng)快速實(shí)現(xiàn)新功能;我認(rèn)為最快的方式就是重構(gòu),所以我就重構(gòu)。
###何時(shí)不應(yīng)該重構(gòu)
聽起來好像我一直在提倡重構(gòu),但確實(shí)有一些不值得重構(gòu)的情況。
如果我看見一塊凌亂的代碼,但并不需要修改它,那么我就不需要重構(gòu)它。如果丑陋的代碼能被隱藏在一個(gè)API之下,我就可以容忍它繼續(xù)保持丑陋。只有當(dāng)我需要理解其工作原理時(shí),對(duì)其進(jìn)行重構(gòu)才有價(jià)值。
另一種情況是,如果重寫比重構(gòu)還容易,就別重構(gòu)了。這是個(gè)困難的決定。如果不花一點(diǎn)兒時(shí)間嘗試,往往很難真實(shí)了解重構(gòu)一塊代碼的難度。決定到底應(yīng)該重構(gòu)還是重寫,需要良好的判斷力與豐富的經(jīng)驗(yàn),我無法給出一條簡(jiǎn)單的建議。
##2.5 重構(gòu)的挑戰(zhàn)
每當(dāng)有人大力推薦一種技術(shù)、工具或者架構(gòu)時(shí),我總是會(huì)觀察這東西會(huì)遇到哪些挑戰(zhàn),畢竟生活中很少有晴空萬里的好事。你需要了解一件事背后的權(quán)衡取舍,才能決定何時(shí)何地應(yīng)用它。我認(rèn)為重構(gòu)是一種很有價(jià)值的技術(shù),大多數(shù)團(tuán)隊(duì)都應(yīng)該更多地重構(gòu),但它也不是完全沒有挑戰(zhàn)的。有必要充分了解重構(gòu)會(huì)遇到的挑戰(zhàn),這樣才能做出有效應(yīng)對(duì)。
###延緩新功能開發(fā)
如果你讀了前面一小節(jié),我對(duì)這個(gè)挑戰(zhàn)的回應(yīng)便已經(jīng)很清楚了。盡管重構(gòu)的目的是加快開發(fā)速度,但是,仍舊很多人認(rèn)為,花在重構(gòu)的時(shí)間是在拖慢新功能的開發(fā)進(jìn)度。“重構(gòu)會(huì)拖慢進(jìn)度”這種看法仍然很普遍,這可能是導(dǎo)致人們沒有充分重構(gòu)的最大阻力所在。
重構(gòu)的唯一目的就是讓我們開發(fā)更快,用更少的工作量創(chuàng)造更大的價(jià)值。
有一種情況確實(shí)需要權(quán)衡取舍。我有時(shí)會(huì)看到一個(gè)(大規(guī)模的)重構(gòu)很有必要進(jìn)行,而馬上要添加的功能非常小,這時(shí)我會(huì)更愿意先把新功能加上,然后再做這次大規(guī)模重構(gòu)。做這個(gè)決定需要判斷力——這是我作為程序員的專業(yè)能力之一。我很難描述決定的過程,更無法量化決定的依據(jù)。
我清楚地知道,預(yù)備性重構(gòu)常會(huì)使修改更容易,所以如果做一點(diǎn)兒重構(gòu)能讓新功能實(shí)現(xiàn)更容易,我一定會(huì)做。如果一個(gè)問題我已經(jīng)見過,此時(shí)我也會(huì)更傾向于重構(gòu)它——有時(shí)我就得先看見一塊丑陋的代碼幾次,然后才能提起勁頭來重構(gòu)它。也就是說,如果一塊代碼我很少觸碰,它不會(huì)經(jīng)常給我?guī)砺闊敲次揖蛢A向于不去重構(gòu)它。如果我還沒想清楚究竟應(yīng)該如何優(yōu)化代碼,那么我可能會(huì)延遲重構(gòu);當(dāng)然,有的時(shí)候,即便沒想清楚優(yōu)化的方向,我也會(huì)先做些實(shí)驗(yàn),試試看能否有所改進(jìn)。
我從同事那里聽到的證據(jù)表明,在我們這個(gè)行業(yè)里,重構(gòu)不足的情況遠(yuǎn)多于重構(gòu)過度的情況。換句話說,絕大多數(shù)人應(yīng)該嘗試多做重構(gòu)。代碼庫(kù)的健康與否,到底會(huì)對(duì)生產(chǎn)率造成多大的影響,很多人可能說不出來,因?yàn)樗麄儧]有太多在健康的代碼庫(kù)上工作的經(jīng)歷——輕松地把現(xiàn)有代碼組合配置,快速構(gòu)造出復(fù)雜的新功能,這種強(qiáng)大的開發(fā)方式他們沒有體驗(yàn)過。
雖然我們經(jīng)常批評(píng)管理者以“保障開發(fā)速度”的名義壓制重構(gòu),其實(shí)程序員自己也經(jīng)常這么干。有時(shí)他們自己覺得不應(yīng)該重構(gòu),其實(shí)他們的領(lǐng)導(dǎo)還挺希望他們做一些重構(gòu)的。如果你是一支團(tuán)隊(duì)的技術(shù)領(lǐng)導(dǎo),一定要向團(tuán)隊(duì)成員表明,你重視改善代碼庫(kù)健康的價(jià)值。合理判斷何時(shí)應(yīng)該重構(gòu)、何時(shí)應(yīng)該暫時(shí)不重構(gòu),這樣的判斷力需要多年經(jīng)驗(yàn)積累。對(duì)于重構(gòu)缺乏經(jīng)驗(yàn)的年輕人需要有意的指導(dǎo),才能幫助他們加速經(jīng)驗(yàn)積累的過程。
有些人試圖用“整潔的代碼”“良好的工程實(shí)踐”之類道德理由來論證重構(gòu)的必要性,我認(rèn)為這是個(gè)陷阱。重構(gòu)的意義不在于把代碼庫(kù)打磨得閃閃發(fā)光,而是純粹經(jīng)濟(jì)角度出發(fā)的考量。我們之所以重構(gòu),因?yàn)樗茏屛覀兏臁砑庸δ芨欤迯?fù)bug更快。一定要隨時(shí)記住這一點(diǎn),與別人交流時(shí)也要不斷強(qiáng)調(diào)這一點(diǎn)。重構(gòu)應(yīng)該總是由經(jīng)濟(jì)利益驅(qū)動(dòng)。程序員、經(jīng)理和客戶越理解這一點(diǎn),“好的設(shè)計(jì)”那條曲線就會(huì)越經(jīng)常出現(xiàn)。
###代碼所有權(quán)
很多重構(gòu)手法不僅會(huì)影響一個(gè)模塊內(nèi)部,還會(huì)影響該模塊與系統(tǒng)其他部分的關(guān)系。比如我想給一個(gè)函數(shù)改名,并且我也能找到該函數(shù)的所有調(diào)用者,那么我只需運(yùn)用改變函數(shù)聲明(124),在一次重構(gòu)中修改函數(shù)聲明和調(diào)用者。但即便這么簡(jiǎn)單的一個(gè)重構(gòu),有時(shí)也無法實(shí)施:調(diào)用方代碼可能由另一支團(tuán)隊(duì)擁有,而我沒有權(quán)限寫入他們的代碼庫(kù);這個(gè)函數(shù)可能是一個(gè)提供給客戶的API,這時(shí)我根本無法知道是否有人使用它,至于誰在用、用得有多頻繁就更是一無所知。這樣的函數(shù)屬于已發(fā)布接口(published interface):接口的使用者(客戶端)與聲明者彼此獨(dú)立,聲明者無權(quán)修改使用者的代碼。
代碼所有權(quán)的邊界會(huì)妨礙重構(gòu),因?yàn)橐坏┪易宰髦鲝埖匦薷模鸵欢〞?huì)破壞使用者的程序。這不會(huì)完全阻止重構(gòu),我仍然可以做很多重構(gòu),但確實(shí)會(huì)對(duì)重構(gòu)造成約束。為了給一個(gè)函數(shù)改名,我需要使用函數(shù)改名(124),但同時(shí)也得保留原來的函數(shù)聲明,使其把調(diào)用傳遞給新的函數(shù)。這會(huì)讓接口變復(fù)雜,但這就是為了避免破壞使用者的系統(tǒng)而不得不付出的代價(jià)。我可以把舊的接口標(biāo)記為“不推薦使用”(deprecated),等一段時(shí)間之后最終讓其退休;但有些時(shí)候,舊的接口必須一直保留下去。
由于這些復(fù)雜性,我建議不要搞細(xì)粒度的強(qiáng)代碼所有制。有些組織喜歡給每段代碼都指定唯一的所有者,只有這個(gè)人能修改這段代碼。我曾經(jīng)見過一支只有三個(gè)人的團(tuán)隊(duì)以這種方式運(yùn)作,每個(gè)程序員都要給另外兩人發(fā)布接口,隨之而來的就是接口維護(hù)的種種麻煩。如果這三個(gè)人都直接去代碼庫(kù)里做修改,事情會(huì)簡(jiǎn)單得多。我推薦團(tuán)隊(duì)代碼所有制,這樣一支團(tuán)隊(duì)里的成員都可以修改這個(gè)團(tuán)隊(duì)擁有的代碼,即便最初寫代碼的是別人。程序員可能各自分工負(fù)責(zé)系統(tǒng)的不同區(qū)域,但這種責(zé)任應(yīng)該體現(xiàn)為監(jiān)控自己責(zé)任區(qū)內(nèi)發(fā)生的修改,而不是簡(jiǎn)單粗暴地禁止別人修改。
這種較為寬容的代碼所有制甚至可以應(yīng)用于跨團(tuán)隊(duì)的場(chǎng)合。有些團(tuán)隊(duì)鼓勵(lì)類似于開源的模型:B團(tuán)隊(duì)的成員也可以在一個(gè)分支上修改A團(tuán)隊(duì)的代碼,然后把提交發(fā)送給A團(tuán)隊(duì)去審核。這樣一來,如果團(tuán)隊(duì)想修改自己的函數(shù),他們就可以同時(shí)修改該函數(shù)的客戶端的代碼;只要客戶端接受了他們的修改,就可以刪掉舊的函數(shù)聲明了。對(duì)于涉及多個(gè)團(tuán)隊(duì)的大系統(tǒng)開發(fā),在“強(qiáng)代碼所有制”和“混亂修改”兩個(gè)極端之間,這種類似開源的模式常常是一個(gè)合適的折中。
###分支
很多團(tuán)隊(duì)采用這樣的版本控制實(shí)踐:每個(gè)團(tuán)隊(duì)成員各自在代碼庫(kù)的一條分支上工作,進(jìn)行相當(dāng)大量的開發(fā)之后,才把各自的修改合并回主線分支(這條分支通常叫master或trunk),從而與整個(gè)團(tuán)隊(duì)分享。常見的做法是在分支上開發(fā)完整的功能,直到功能可以發(fā)布到生產(chǎn)環(huán)境,才把該分支合并回主線。這種做法的擁躉聲稱,這樣能保持主線不受尚未完成的代碼侵?jǐn)_,能保留清晰的功能添加的版本記錄,并且在某個(gè)功能出問題時(shí)能容易地撤銷修改。
這樣的特性分支有其缺點(diǎn)。在隔離的分支上工作得越久,將完成的工作集成(integrate)回主線就會(huì)越困難。為了減輕集成的痛苦,大多數(shù)人的辦法是頻繁地從主線合并(merge)或者變基(rebase)到分支。但如果有幾個(gè)人同時(shí)在各自的特性分支上工作,這個(gè)辦法并不能真正解決問題,因?yàn)楹喜⑴c集成是兩回事。如果我從主線合并到我的分支,這只是一個(gè)單向的代碼移動(dòng)——我的分支發(fā)生了修改,但主線并沒有。而“集成”是一個(gè)雙向的過程:不僅要把主線的修改拉(pull)到我的分支上,而且要把我這里修改的結(jié)果推(push)回到主線上,兩邊都會(huì)發(fā)生修改。假如另一名程序員Rachel正在她的分支上開發(fā),我是看不見她的修改的,直到她將自己的修改與主線集成;此時(shí)我就必須把她的修改合并到我的特性分支,這可能需要相當(dāng)?shù)墓ぷ髁俊F渲欣щy的部分是處理語(yǔ)義變化。現(xiàn)代版本控制系統(tǒng)都能很好地合并程序文本的復(fù)雜修改,但對(duì)于代碼的語(yǔ)義它們一無所知。如果我修改了一個(gè)函數(shù)的名字,版本控制工具可以很輕松地將我的修改與Rachel的代碼集成。但如果在集成之前,她在自己的分支里新添調(diào)用了這個(gè)被我改名的函數(shù),集成之后的代碼就會(huì)被破壞。
分支合并本來就是一個(gè)復(fù)雜的問題,隨著特性分支存在的時(shí)間加長(zhǎng),合并的難度會(huì)指數(shù)上升。集成一個(gè)已經(jīng)存在了4個(gè)星期的分支,較之集成存在了2個(gè)星期的分支,難度可不止翻倍。所以很多人認(rèn)為,應(yīng)該盡量縮短特性分支的生存周期,比如只有一兩天。還有一些人(比如我本人)認(rèn)為特性分支的生命還應(yīng)該更短,我們采用的方法叫作持續(xù)集成(Continuous Integration,CI),也叫“基于主干開發(fā)”(Trunk-Based Development)。在使用CI時(shí),每個(gè)團(tuán)隊(duì)成員每天至少向主線集成一次。這個(gè)實(shí)踐避免了任何分支彼此差異太大,從而極大地降低了合并的難度。不過CI也有其代價(jià):你必須使用相關(guān)的實(shí)踐以確保主線隨時(shí)處于健康狀態(tài),必須學(xué)會(huì)將大功能拆分成小塊,還必須使用特性開關(guān)(feature toggle,也叫特性旗標(biāo),feature flag)將尚未完成又無法拆小的功能隱藏掉。
CI的粉絲之所以喜歡這種工作方式,部分原因是它降低了分支合并的難度,不過最重要的原因還是CI與重構(gòu)能良好配合。重構(gòu)經(jīng)常需要對(duì)代碼庫(kù)中的很多地方做很小的修改(例如給一個(gè)廣泛使用的函數(shù)改名),這樣的修改尤其容易造成合并時(shí)的語(yǔ)義沖突。采用特性分支的團(tuán)隊(duì)常會(huì)發(fā)現(xiàn)重構(gòu)加劇了分支合并的困難,并因此放棄了重構(gòu),這種情況我們?cè)?jīng)見過多次。CI和重構(gòu)能夠良好配合,所以Kent Beck在極限編程中同時(shí)包含了這兩個(gè)實(shí)踐。
我并不是在說絕不應(yīng)該使用特性分支。如果特性分支存在的時(shí)間足夠短,它們就不會(huì)造成大問題。(實(shí)際上,使用CI的團(tuán)隊(duì)往往同時(shí)也使用分支,但他們會(huì)每天將分支與主線合并。)對(duì)于開源項(xiàng)目,特性分支可能是合適的做法,因?yàn)椴粫r(shí)會(huì)有你不熟悉(因此也不信任)的程序員偶爾提交修改。但對(duì)全職的開發(fā)團(tuán)隊(duì)而言,特性分支對(duì)重構(gòu)的阻礙太嚴(yán)重了。即便你沒有完全采用CI,我也一定會(huì)催促你盡可能頻繁地集成。而且,用上CI的團(tuán)隊(duì)在軟件交付上更加高效,我真心希望你認(rèn)真考慮這個(gè)客觀事實(shí)[Forsgren et al]。
###測(cè)試
不會(huì)改變程序可觀察的行為,這是重構(gòu)的一個(gè)重要特征。如果仔細(xì)遵循重構(gòu)手法的每個(gè)步驟,我應(yīng)該不會(huì)破壞任何東西,但萬一我犯了個(gè)錯(cuò)誤怎么辦?(呃,就我這個(gè)粗心大意的性格來說,請(qǐng)去掉“萬一”兩字。)人總會(huì)有出錯(cuò)的時(shí)候,不過只要及時(shí)發(fā)現(xiàn),就不會(huì)造成大問題。既然每個(gè)重構(gòu)都是很小的修改,即便真的造成了破壞,我也只需要檢查最后一步的小修改——就算找不到出錯(cuò)的原因,只要回滾到版本控制中最后一個(gè)可用的版本就行了。
這里的關(guān)鍵就在于“快速發(fā)現(xiàn)錯(cuò)誤”。要做到這一點(diǎn),我的代碼應(yīng)該有一套完備的測(cè)試套件,并且運(yùn)行速度要快,否則我會(huì)不愿意頻繁運(yùn)行它。也就是說,絕大多數(shù)情況下,如果想要重構(gòu),我得先有可以自測(cè)試的代碼[mf-stc]。
有些讀者可能會(huì)覺得,“自測(cè)試的代碼”這個(gè)要求太高,根本無法實(shí)現(xiàn)。但在過去20年中,我看到很多團(tuán)隊(duì)以這種方式構(gòu)造軟件。的確,團(tuán)隊(duì)必須投入時(shí)間與精力在測(cè)試上,但收益是絕對(duì)劃算的。自測(cè)試的代碼不僅使重構(gòu)成為可能,而且使添加新功能更加安全,因?yàn)槲铱梢院芸彀l(fā)現(xiàn)并干掉新近引入的bug。這里的關(guān)鍵在于,一旦測(cè)試失敗,我只需要查看上次測(cè)試成功運(yùn)行之后修改的這部分代碼;如果測(cè)試運(yùn)行得很頻繁,這個(gè)查看的范圍就只有幾行代碼。知道必定是這幾行代碼造成bug的話,排查起來會(huì)容易得多。
這也回答了“重構(gòu)風(fēng)險(xiǎn)太大,可能引入bug”的擔(dān)憂。如果沒有自測(cè)試的代碼,這種擔(dān)憂就是完全合理的,這也是為什么我如此重視可靠的測(cè)試。
缺乏測(cè)試的問題可以用另一種方式來解決。如果我的開發(fā)環(huán)境很好地支持自動(dòng)化重構(gòu),我就可以信任這些重構(gòu),不必運(yùn)行測(cè)試。這時(shí)即便沒有完備的測(cè)試套件,我仍然可以重構(gòu),前提是僅僅使用那些自動(dòng)化的、一定安全的重構(gòu)手法。這會(huì)讓我損失很多好用的重構(gòu)手法,不過剩下可用的也不少,我還是能從中獲益。當(dāng)然,我還是更愿意有自測(cè)試的代碼,但如果沒有,自動(dòng)化重構(gòu)的工具包也很好。
缺乏測(cè)試的現(xiàn)狀還催生了另一種重構(gòu)的流派:只使用一組經(jīng)過驗(yàn)證是安全的重構(gòu)手法。這個(gè)流派要求嚴(yán)格遵循重構(gòu)的每個(gè)步驟,并且可用的重構(gòu)手法是特定于語(yǔ)言的。使用這種方法,團(tuán)隊(duì)得以在測(cè)試覆蓋率很低的大型代碼庫(kù)上開展一些有用的重構(gòu)。這個(gè)重構(gòu)流派比較新,涉及一些很具體、特定于編程語(yǔ)言的技巧與做法,行業(yè)里對(duì)這種方法的介紹和了解都還不足,因此本書不對(duì)其多做介紹。(不過我希望未來在我自己的網(wǎng)站上多討論這個(gè)主題。感興趣的讀者可以查看Jay Bazuzi關(guān)于如何在C++中安全地運(yùn)用提煉函數(shù)(106)的描述[Bazuzi],借此獲得一點(diǎn)兒對(duì)這個(gè)重構(gòu)流派的了解。)
毫不意外,自測(cè)試代碼與持續(xù)集成緊密相關(guān)——我們仰賴持續(xù)集成來及時(shí)捕獲分支集成時(shí)的語(yǔ)義沖突。自測(cè)試代碼是極限編程的另一個(gè)重要組成部分,也是持續(xù)交付的關(guān)鍵環(huán)節(jié)。
###遺留代碼
大多數(shù)人會(huì)覺得,有一大筆遺產(chǎn)是件好事,但從程序員的角度來看就不同了。遺留代碼往往很復(fù)雜,測(cè)試又不足,而且最關(guān)鍵的是,是別人寫的(瑟瑟發(fā)抖)。
重構(gòu)可以很好地幫助我們理解遺留系統(tǒng)。引人誤解的函數(shù)名可以改名,使其更好地反映代碼用途;糟糕的程序結(jié)構(gòu)可以慢慢理順,把程序從一塊頑石打磨成美玉。整個(gè)故事都很棒,但我們繞不開關(guān)底的惡龍:遺留系統(tǒng)多半沒測(cè)試。如果你面對(duì)一個(gè)龐大而又缺乏測(cè)試的遺留系統(tǒng),很難安全地重構(gòu)清理它。
對(duì)于這個(gè)問題,顯而易見的答案是“沒測(cè)試就加測(cè)試”。這事聽起來簡(jiǎn)單(當(dāng)然工作量必定很大),操作起來可沒那么容易。一般來說,只有在設(shè)計(jì)系統(tǒng)時(shí)就考慮到了測(cè)試,這樣的系統(tǒng)才容易添加測(cè)試——可要是如此,系統(tǒng)早該有測(cè)試了,我也不用操這份心了。
這個(gè)問題沒有簡(jiǎn)單的解決辦法,我能給出的最好建議就是買一本《修改代碼的藝術(shù)》[Feathers],照書里的指導(dǎo)來做。別擔(dān)心那本書太老,盡管已經(jīng)出版十多年,其中的建議仍然管用。一言以蔽之,它建議你先找到程序的接縫,在接縫處插入測(cè)試,如此將系統(tǒng)置于測(cè)試覆蓋之下。你需要運(yùn)用重構(gòu)手法創(chuàng)造出接縫——這樣的重構(gòu)很危險(xiǎn),因?yàn)闆]有測(cè)試覆蓋,但這是為了取得進(jìn)展必要的風(fēng)險(xiǎn)。在這種情況下,安全的自動(dòng)化重構(gòu)簡(jiǎn)直就是天賜福音。如果這一切聽起來很困難,因?yàn)樗_實(shí)很困難。很遺憾,一旦跌進(jìn)這個(gè)深坑,沒有爬出來的捷徑,這也是我強(qiáng)烈倡導(dǎo)從一開始就寫能自測(cè)試的代碼的原因。
就算有了測(cè)試,我也不建議你嘗試一鼓作氣把復(fù)雜而混亂的遺留代碼重構(gòu)成漂亮的代碼。我更愿意隨時(shí)重構(gòu)相關(guān)的代碼:每次觸碰一塊代碼時(shí),我會(huì)嘗試把它變好一點(diǎn)點(diǎn)——至少要讓營(yíng)地比我到達(dá)時(shí)更干凈。如果是一個(gè)大系統(tǒng),越是頻繁使用的代碼,改善其可理解性的努力就能得到越豐厚的回報(bào)。
###數(shù)據(jù)庫(kù)
在本書的第1版中,我說過數(shù)據(jù)庫(kù)是“重構(gòu)經(jīng)常出問題的一個(gè)領(lǐng)域”。然而在第1版問世之后僅僅一年,情況就發(fā)生了改變:我的同事Pramod Sadalage發(fā)展出一套漸進(jìn)式數(shù)據(jù)庫(kù)設(shè)計(jì)[mf-evodb]和數(shù)據(jù)庫(kù)重構(gòu)[Ambler & Sadalage]的辦法,如今已經(jīng)被廣泛使用。這項(xiàng)技術(shù)的精要在于:借助數(shù)據(jù)遷移腳本,將數(shù)據(jù)庫(kù)結(jié)構(gòu)的修改與代碼相結(jié)合,使大規(guī)模的、涉及數(shù)據(jù)庫(kù)的修改可以比較容易地開展。
假設(shè)我們要對(duì)一個(gè)數(shù)據(jù)庫(kù)字段(列)改名。和改變函數(shù)聲明(124)一樣,我要找出結(jié)構(gòu)的聲明處和所有調(diào)用處,然后一次完成所有修改。但這里的復(fù)雜之處在于,原來基于舊字段的數(shù)據(jù),也要轉(zhuǎn)為使用新字段。我會(huì)寫一小段代碼來執(zhí)行數(shù)據(jù)轉(zhuǎn)化的邏輯,并把這段代碼放進(jìn)版本控制,跟數(shù)據(jù)結(jié)構(gòu)聲明與使用代碼的修改一并提交。此后如果我想把數(shù)據(jù)庫(kù)遷移到某個(gè)版本,只要執(zhí)行當(dāng)前數(shù)據(jù)庫(kù)版本與目標(biāo)版本之間的所有遷移腳本即可。
跟通常的重構(gòu)一樣,數(shù)據(jù)庫(kù)重構(gòu)的關(guān)鍵也是小步修改并且每次修改都應(yīng)該完整,這樣每次遷移之后系統(tǒng)仍然能運(yùn)行。由于每次遷移涉及的修改都很小,寫起來應(yīng)該容易;將多個(gè)遷移串聯(lián)起來,就能對(duì)數(shù)據(jù)庫(kù)結(jié)構(gòu)及其中存儲(chǔ)的數(shù)據(jù)做很大的調(diào)整。
與常規(guī)的重構(gòu)不同,很多時(shí)候,數(shù)據(jù)庫(kù)重構(gòu)最好是分散到多次生產(chǎn)發(fā)布來完成,這樣即便某次修改在生產(chǎn)數(shù)據(jù)庫(kù)上造成了問題,也比較容易回滾。比如,要改名一個(gè)字段,我的第一次提交會(huì)新添一個(gè)字段,但暫時(shí)不使用它。然后我會(huì)修改數(shù)據(jù)寫入的邏輯,使其同時(shí)寫入新舊兩個(gè)字段。隨后我就可以修改讀取數(shù)據(jù)的地方,將它們逐個(gè)改為使用新字段。這步修改完成之后,我會(huì)暫停一小段時(shí)間,看看是否有bug冒出來。確定沒有bug之后,我再刪除已經(jīng)沒人使用的舊字段。這種修改數(shù)據(jù)庫(kù)的方式是并行修改(Parallel Change,也叫擴(kuò)展協(xié)議/expand-contract)[mf-pc]的一個(gè)實(shí)例。
本文轉(zhuǎn)載自異步社區(qū)
軟件開發(fā)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。