What:何為重構(gòu)
使用一系列重構(gòu)準(zhǔn)則又厉,在不改變「軟件之可察行為」前提下校摩,優(yōu)化代碼結(jié)構(gòu)几睛。
軟件開發(fā)的時間會分配給兩種行為:添加新功能和重構(gòu)败晴。
在添加新功能時浓冒,不應(yīng)該修改既有代碼,直觀添加新功能尖坤,通過測試稳懒。
而重構(gòu)時,你就不能再添加新功能慢味,只管改進(jìn)程序結(jié)構(gòu)场梆,而盡量不修改測試case。
重構(gòu)是這樣一個過程:它在一個目前可運行的程序上進(jìn)行纯路,企圖在「不改變程序行為」的情況下賦予上述美好性質(zhì)或油,使我們能夠繼續(xù)保持高速開發(fā),從而增加程序的價值驰唬。
Why:為何重構(gòu)
缺乏對過程的精心設(shè)計與必要投入顶岸,只抱著對結(jié)果的美好憧憬提刀上陣,遇到困難就靠“奮斗精神”和加班解決定嗓,這種“刀劈斧砍”不止發(fā)生在缺乏審慎的“重構(gòu)”現(xiàn)場蜕琴,又何嘗不是我們這個行業(yè)的縮影?
程序有兩面價值:「今天可以為你做什么」和「明天可以為你做什么」宵溅。大多數(shù)時候凌简,我們都只關(guān)注自己今天想要程序做什么。不論是修復(fù)錯誤或是添加特性恃逻,我們都是為了讓程序力更強雏搂,讓它在今天更有價值。對于今天的工作寇损,我了解得很充分:對于明天的工作凸郑,我了解得不夠充分。但如果我純粹只是為今天工作矛市,明天我將完全無法工作芙沥。好代碼的檢驗標(biāo)準(zhǔn)是人們能否輕而易舉修改它。
「重構(gòu)」改進(jìn)軟件設(shè)計
「重構(gòu)」使軟件更易被理解
「重構(gòu)」助你找到Bug
「重構(gòu)」助你提高編程速度
為什么開發(fā)者不愿意重構(gòu)他們的程序?有幾個可能的原因:
你不知道如何重構(gòu)而昨。
如果這些利益是長遠(yuǎn)才展現(xiàn)的救氯,何必現(xiàn)在付出這些努力呢?長遠(yuǎn)看來歌憨,說不定當(dāng)項目收獲這些利益時着憨,你已經(jīng)不在職位上了。
代碼重構(gòu)是一項額外工作务嫡,老板付錢給你甲抖,主要是讓你編寫新功能。
重構(gòu)可能破壞現(xiàn)有程序心铃。
重構(gòu)的重要性
拿我們都非常熟悉的一件事來做個比喻吧:我們的身體健康狀況准谚。
從很多角度來說,重構(gòu)就好像運動于个、吃適當(dāng)?shù)氖澄锓湛TS多人都知道:我們應(yīng)該多鍛煉身體,應(yīng)該注意均衡飲食厅篓。有些人的生活文化中非常鼓勵這些習(xí)慣秀存,有些人沒有這些好習(xí)慣也可以混過一段時間,甚至看不出有什么影響羽氮。我們可以找各種借口或链,
但如果一直忽視這些好習(xí)慣,那么我們只是在欺騙自己档押。 有些人之運動和均衡飲食澳盐,動機(jī)著眼于短期利益(例如精力更充沛、身體更靈活令宿、
自尊心增強……等等)叼耙。幾乎所有人都知道這些短期利益非常真實。許多人都時斷時續(xù)做過一些努力粒没,另一些人則是不見棺材不掉淚筛婉,不到關(guān)鍵時刻不會有足夠動力去做點什么事。
When:何時重構(gòu)
三次法則:事不過三癞松,三則重構(gòu)
添加功能時一并重構(gòu)
修補錯誤吋一并重構(gòu)
復(fù)審代碼吋一并重構(gòu)
何吋不該重構(gòu)爽撒?
如果還沒有徹底理解既有的代碼,丑陋的代碼能被隱藏在一個API之下响蓉,我就可以容忍它繼續(xù)保持丑陋硕勿。只有當(dāng)我需要理解其工作原理時,對其進(jìn)行重構(gòu)才有價值枫甲。
另一種情況是源武,如果重寫比重構(gòu)還容易扼褪,就別重構(gòu)了。
另外粱栖,如果項目已近最后期限迎捺,你也應(yīng)該避免重構(gòu)。在此時機(jī)查排,從重構(gòu)過程贏得的生產(chǎn)力只有在最后期限過后才能體現(xiàn)出來,而那個時候已經(jīng)時不我予抄沮。把未完成的重構(gòu)工作形容為「債務(wù)」跋核。很多公司都需要借債來使自己更有效地運轉(zhuǎn)。但是借債就得付利息叛买,過于復(fù)雜的代碼所造成的「維護(hù)和擴(kuò)展的額外開銷」就是利息砂代。你可以承受一定程度的利息,但如果利息太高你就會被壓垮率挣。把債務(wù)管理好是很重要的刻伊,你應(yīng)該隨時通過重構(gòu)來償還一部分債務(wù)。
如果項目己經(jīng)非常接近最后期限椒功,你不應(yīng)該再分心于重構(gòu)捶箱,因為己經(jīng)沒有時間了。不過多個項目經(jīng)驗顯示:重構(gòu)的確能夠提高生產(chǎn)力动漾。如果最后你沒有足夠時間丁屎,通常就表示你其實早該進(jìn)行重構(gòu)。
HOW:如何重構(gòu)
我們希望程序:
(1)容易理解旱眯;
(2)所有邏輯都只在唯一地點指定晨川;
(3)新的改動不會危及現(xiàn)有行為;
(4)盡可能簡單表達(dá)條件邏輯(conditional logic)删豺。
詳見《重構(gòu)-改善既有代碼的設(shè)計》第一版共虑,第二版。
重構(gòu)中常見的重要問題
「該怎么跟經(jīng)理說重構(gòu)的事呀页?」
如果這位經(jīng)理懂技術(shù)妈拌,那么向他介紹重構(gòu)應(yīng)該不會很困難泛粹。如果這位經(jīng)理只對質(zhì)量感興趣涣达,那么問題就集中到了「質(zhì)量」上面。此時妙啃,在復(fù)審過程中使用重構(gòu)疾党,就是一個不錯的辦法音诫。
大量研究結(jié)果顯示,「技術(shù)復(fù)審」是減少錯誤雪位、提高開發(fā)速度的一條重要途徑竭钝。隨便找一本關(guān)于復(fù)審、審査或軟件開發(fā)程序的書看看,從中找些最新引證香罐,應(yīng)該可以讓大多數(shù)經(jīng)理認(rèn)識復(fù)審的價值卧波。然后你就可以把重構(gòu)當(dāng)作「將復(fù)審意見引入代碼內(nèi)」的方法來使用,這很容易庇茫。
當(dāng)然港粱,很多經(jīng)理嘴巴上說自己「質(zhì)量驅(qū)動」,其實更多是「進(jìn)度驅(qū)動」旦签。這種情況下我會給他們一個較有爭議的建議:不要告訴經(jīng)理查坪!
「接口迭代問題」
原則:不要過早發(fā)布(publish)接口。修改代碼擁有權(quán)政策宁炫,使重構(gòu)更順暢偿曙。
很多重構(gòu)手法不僅會影響一個模塊內(nèi)部,還會影響該模塊與系統(tǒng)其他部分的關(guān)系羔巢。比如我想給一個函數(shù)改名望忆,并且我也能找到該函數(shù)的所有調(diào)用者,那么我只需運用改變函數(shù)聲明竿秆,在一次重構(gòu)中修改函數(shù)聲明和調(diào)用者启摄。但即便這么簡單的一個重構(gòu),有時也無法實施:調(diào)用方代碼可能由另一支團(tuán)隊擁有袍辞,而我沒有權(quán)限寫入他們的代碼庫鞋仍;這個函數(shù)可能是一個提供給客戶的API,這時我根本無法知道是否有人使用它搅吁,至于誰在用威创、用得有多頻繁就更是一無所知。這樣的函數(shù)屬于已發(fā)布接口:接口的使用者(客戶端)與聲明者彼此獨立谎懦,聲明者無權(quán)修改使用者的代碼肚豺。
代碼所有權(quán)的邊界會妨礙重構(gòu),因為一旦我自作主張地修改界拦,就一定會破壞使用者的程序吸申。這不會完全阻止重構(gòu),我仍然可以做很多重構(gòu)享甸,但確實會對重構(gòu)造成約束截碴。為了給一個函數(shù)改名,我需要使用函數(shù)改名蛉威,但同時也得保留原來的函數(shù)聲明日丹,使其把調(diào)用傳遞給新的函數(shù)。這會讓接口變復(fù)雜蚯嫌,但這就是為了避免破壞使用者的系統(tǒng)而不得不付出的代價哲虾。我可以把舊的接口標(biāo)記為“不推薦使用”(deprecated)丙躏,等一段時間之后最終讓其退休;但有些時候束凑,舊的接口必須一直保留下去晒旅。
由于這些復(fù)雜性,我建議不要搞細(xì)粒度的強代碼所有制汪诉。有些組織喜歡給每段代碼都指定唯一的所有者废恋,只有這個人能修改這段代碼。我曾經(jīng)見過一支只有三個人的團(tuán)隊以這種方式運作扒寄,每個程序員都要給另外兩人發(fā)布接口拴签,隨之而來的就是接口維護(hù)的種種麻煩。如果這三個人都直接去代碼庫里做修改旗们,事情會簡單得多。我推薦團(tuán)隊代碼所有制构灸,這樣一支團(tuán)隊里的成員都可以修改這個團(tuán)隊擁有的代碼上渴,即便最初寫代碼的是別人。程序員可能各自分工負(fù)責(zé)系統(tǒng)的不同區(qū)域喜颁,但這種責(zé)任應(yīng)該體現(xiàn)為監(jiān)控自己責(zé)任區(qū)內(nèi)發(fā)生的修改稠氮,而不是簡單粗暴地禁止別人修改。
這種較為寬容的代碼所有制甚至可以應(yīng)用于跨團(tuán)隊的場合半开。有些團(tuán)隊鼓勵類似于開源的模型:B團(tuán)隊的成員也可以在一個分支上修改A團(tuán)隊的代碼隔披,然后把提交發(fā)送給A團(tuán)隊去審核。這樣一來寂拆,如果團(tuán)隊想修改自己的函數(shù)奢米,他們就可以同時修改該函數(shù)的客戶端的代碼;只要客戶端接受了他們的修改纠永,就可以刪掉舊的函數(shù)聲明了鬓长。對于涉及多個團(tuán)隊的大系統(tǒng)開發(fā),在“強代碼所有制”和“混亂修改”兩個極端之間尝江,這種類似開源的模式常常是一個合適的折中涉波。
PS1: 這也是3.0Plus項目初期和中期架構(gòu)組一直堅持源碼依賴的原因,大部分接口變動的情況炭序,是依靠架構(gòu)組同學(xué)直接修改提交使用者的代碼來完成的接口迭代啤覆,保證快速迭代的同時避免編譯報錯。
PS2: 這也是3.0Plus項目后期惭聂,底層庫全部切換為Maven依賴窗声。上ViewFocus功能的時候,被項目要求1天定好ViewFocus的最終接口并宣講彼妻,之后再開發(fā)ViewFocus功能嫌佑。索幸接口之后沒有沒有大的變動豆茫。
「分支問題」
很多團(tuán)隊采用這樣的版本控制實踐:
每個團(tuán)隊成員各自在代碼庫的一條分支上工作,進(jìn)行相當(dāng)大量的開發(fā)之后屋摇,才把各自的修改合并回主線分支(這條分支通常叫master或trunk)揩魂,從而與整個團(tuán)隊分享。常見的做法是在分支上開發(fā)完整的功能炮温,直到功能可以發(fā)布到生產(chǎn)環(huán)境火脉,才把該分支合并回主線。這種做法的擁躉聲稱柒啤,這樣能保持主線不受尚未完成的代碼侵?jǐn)_倦挂,能保留清晰的功能添加的版本記錄,并且在某個功能出問題時能容易地撤銷修改担巩。
這樣的特性分支有其缺點方援。在隔離的分支上工作得越久,將完成的工作集成(integrate)回主線就會越困難涛癌。為了減輕集成的痛苦犯戏,大多數(shù)人的辦法是頻繁地從主線合并(merge)或者變基(rebase)到分支。但如果有幾個人同時在各自的特性分支上工作拳话,這個辦法并不能真正解決問題先匪,因為合并與集成是兩回事。如果我從主線合并到我的分支弃衍,這只是一個單向的代碼移動——我的分支發(fā)生了修改呀非,但主線并沒有。而“集成”是一個雙向的過程:不僅要把主線的修改拉(pull)到我的分支上镜盯,而且要把我這里修改的結(jié)果推(push)回到主線上岸裙,兩邊都會發(fā)生修改。假如另一名程序員正在他的分支上開發(fā)速缆,我是看不見他的修改的哥桥,直到他將自己的修改與主線集成;此時我就必須把她的修改合并到我的特性分支激涤,這可能需要相當(dāng)?shù)墓ぷ髁磕飧狻F渲欣щy的部分是處理語義變化。現(xiàn)代版本控制系統(tǒng)都能很好地合并程序文本的復(fù)雜修改倦踢,但對于代碼的語義它們一無所知送滞。如果我修改了一個函數(shù)的名字,版本控制工具可以很輕松地將我的修改與他的代碼集成辱挥。但如果在集成之前犁嗅,他在自己的分支里新添調(diào)用了這個被我改名的函數(shù),集成之后的代碼就會被破壞晤碘。
分支合并本來就是一個復(fù)雜的問題褂微,隨著特性分支存在的時間加長功蜓,合并的難度會指數(shù)上升。集成一個已經(jīng)存在了4個星期的分支宠蚂,較之集成存在了2個星期的分支式撼,難度可不止翻倍。
所以很多人認(rèn)為求厕,應(yīng)該盡量縮短特性分支的生存周期著隆,比如只有一兩天。還有一些人(比如我本人)認(rèn)為特性分支的生命還應(yīng)該更短呀癣,我們采用的方法叫作持續(xù)集成(Continuous Integration美浦,CI),也叫“基于主干開發(fā)”(Trunk-Based Development)项栏。在使用CI時浦辨,每個團(tuán)隊成員每天至少向主線集成一次。這個實踐避免了任何分支彼此差異太大沼沈,從而極大地降低了合并的難度荤牍。不過CI也有其代價:你必須使用相關(guān)的實踐以確保主線隨時處于健康狀態(tài),必須學(xué)會將大功能拆分成小塊庆冕,還必須使用特性開關(guān)(feature toggle,也叫特性旗標(biāo)劈榨,feature flag)將尚未完成又無法拆小的功能隱藏掉访递。 CI的粉絲之所以喜歡這種工作方式,部分原因是它降低了分支合并的難度同辣,不過最重要的原因還是CI與重構(gòu)能良好配合拷姿。重構(gòu)經(jīng)常需要對代碼庫中的很多地方做很小的修改(例如給一個廣泛使用的函數(shù)改名),這樣的修改尤其容易造成合并時的語義沖突旱函。采用特性分支的團(tuán)隊常會發(fā)現(xiàn)重構(gòu)加劇了分支合并的困難响巢,并因此放棄了重構(gòu),這種情況我們曾經(jīng)見過多次棒妨。CI和重構(gòu)能夠良好配合踪古,所以Kent Beck在極限編程中同時包含了這兩個實踐。 我并不是在說絕不應(yīng)該使用特性分支券腔。如果特性分支存在的時間足夠短伏穆,它們就不會造成大問題。(實際上纷纫,使用CI的團(tuán)隊往往同時也使用分支枕扫,但他們會每天將分支與主線合并。)對于開源項目辱魁,特性分支可能是合適的做法烟瞧,因為不時會有你不熟悉(因此也不信任)的程序員偶爾提交修改诗鸭。但對全職的開發(fā)團(tuán)隊而言,特性分支對重構(gòu)的阻礙太嚴(yán)重了参滴。即便你沒有完全采用CI强岸,我也一定會催促你盡可能頻繁地集成。而且卵洗,用上CI的團(tuán)隊在軟件交付上更加高效请唱,我真心希望你認(rèn)真考慮這個客觀事實。