
背景
《重構(gòu)》誕生至今有近17個(gè)年頭了,日常開發(fā)中大家談到重構(gòu),要么非常隨意,認(rèn)為重構(gòu)就是改代碼;要么非常謹(jǐn)慎,把重構(gòu)描述成焦油坑,像瘟神一樣敬而遠(yuǎn)之。我們應(yīng)該怎么看重構(gòu)呢?針對(duì)最具挑戰(zhàn)的遺留代碼的重構(gòu),有哪些需要注意的呢?
談?wù)撊魏问虑?,都該有它的上下文。本文談?wù)摰募夹g(shù)背景是大型通信類產(chǎn)品,對(duì)于互聯(lián)網(wǎng)產(chǎn)品不一定適用。另外,本文也不會(huì)涉及重構(gòu)技術(shù),有興趣讀者可以讀《重構(gòu)》或者《Effective Refactoring in C++》。
重構(gòu)與收拾屋子
為什么收拾屋子?
住酒店,有服務(wù)生幫我們收拾房間,在家需要自己收拾,因?yàn)樽约哼€要住很長時(shí)間。屋子干凈了,還是有好處的,東西就好找了,哪些東西放的位置不對(duì),也更容易識(shí)別出來。
重構(gòu)亦是。我們看看《重構(gòu)》中Martin Fowler對(duì)“重構(gòu)”的定義:
名詞定義:對(duì)軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。
定義中給出了重構(gòu)的兩個(gè)目的(收益):
- 提高可理解性
- 降低修改成本
代碼編寫完成后,自己或者他人還會(huì)維護(hù)較長一段時(shí)間。重構(gòu),是期望代碼可以更多的被復(fù)用,有更長的服役時(shí)間。
如何收拾屋子?
根據(jù)屋子的布置(結(jié)構(gòu)),每件物品都有它應(yīng)該放置的位置,發(fā)現(xiàn)不在位置上,就調(diào)整調(diào)整。
重構(gòu)亦是。重構(gòu)是對(duì)軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整。好的軟件在滿足系統(tǒng)功能正確性、性能等要素同時(shí),還需考慮軟件的擴(kuò)展性、伸縮性和可讀性。所以它應(yīng)該是有架構(gòu)的,體現(xiàn)為橫向切分的不同層次和縱向切分的不同模塊,及更細(xì)粒度的類、方法、函數(shù)。重構(gòu)就是根據(jù)軟件本身應(yīng)該的結(jié)構(gòu),對(duì)代碼元素進(jìn)行調(diào)整,放到合適的位置。
什么時(shí)候收拾?
發(fā)現(xiàn)屋子臟了、亂了就收拾收拾,工作忙,就周末收拾,工作輕松些就每天順手收拾。但是房子臟,那是欠的債,遲早需要還的。
重構(gòu)亦是。伴隨著開發(fā)的過程,重構(gòu)應(yīng)該是個(gè)習(xí)慣,發(fā)現(xiàn)代碼有壞味道了,就及時(shí)消除掉。交付壓力太大時(shí),就稍微緩緩,等稍能喘口氣,就趕緊把債還掉,不然積少成多,可能就很難收拾了。
Martin Flower 給出了四個(gè)重構(gòu)時(shí)機(jī):
Kent Beck提出的XP(eXtreme Programming)中,TDD(Test Driven Development)實(shí)踐更是把重構(gòu)作為開發(fā)過程中的一部分:
- 產(chǎn)生重復(fù)代碼時(shí),
- 新增功能之前
- 修改故障之前
- 代碼走查之后

逢年過節(jié),你會(huì)來個(gè)大掃除,徹底把屋子調(diào)整調(diào)整。
重構(gòu)亦是。時(shí)間久了,新人多了,交付壓力大了,代碼難免會(huì)產(chǎn)生一些腐化,這些可以通過集中重構(gòu),徹底清洗清洗。
發(fā)現(xiàn)房間布置,格調(diào)有點(diǎn)跟不上時(shí)代,需要整的大些,就是裝修了。要看自己有沒有地兒住,有沒有時(shí)間折騰。
重構(gòu)亦是。當(dāng)系統(tǒng)新增功能改動(dòng)很大;當(dāng)系統(tǒng)性能成為瓶頸,無法忍受;當(dāng)代碼穿著一層層補(bǔ)丁貼成的外衣,仍到處漏風(fēng)時(shí),此時(shí)縫縫補(bǔ)補(bǔ)此時(shí)已無濟(jì)于事,需要更大的調(diào)整,才能根除這些問題。當(dāng)然此時(shí)系統(tǒng)已經(jīng)上線,用戶嗷嗷待哺,你需要有替代方案過度,需要爭取時(shí)間調(diào)整。
再看一則小故事:
一個(gè)建筑隊(duì),全國各地到處跑,住的是一個(gè)茅草屋。遮風(fēng)避雨是沒有問題的,只是每到一地都要重蓋,一到下大雨還到處漏水。修修補(bǔ)補(bǔ)的辦法總有的,但總是很狼狽。有一天,有人告訴工程隊(duì),可以用可組合的鐵框?yàn)楣羌?,彩鋼為夾層,這樣既可以連續(xù)拆裝,節(jié)省成本,房子也結(jié)實(shí)牢固。 工人們開始擔(dān)心了:“怎么組裝啊,看上去好麻煩!”。工程隊(duì)的老人也替工頭擔(dān)心,“那得花多少錢啊”,“會(huì)不會(huì)住不習(xí)慣”,工頭出去考察了一番,回來一狠心,整了一套,雖然過程有些費(fèi)勁,結(jié)果收益還是杠杠的。后來大家?guī)缀醵纪嗽?jīng)住過茅草屋。
當(dāng)然,這里講的不再是重構(gòu)了,這種推倒重來的做法,我們叫它再工程(re-engineering)。與重構(gòu)相比,他的風(fēng)險(xiǎn)更大,成本更高。但所謂高風(fēng)險(xiǎn),高回報(bào),如果它能帶來更大的收益,甚至顛覆性創(chuàng)新,我們也值得去做。
再工程不是本文討論的重點(diǎn),打??!
遺留代碼重構(gòu)
遺留代碼的重構(gòu)屬于上文中提到的“大掃除”或“裝修”場景。對(duì)遺留代碼進(jìn)行重構(gòu),很容易形成“吃力不討好”的局面,究其原因,我們先回顧下重構(gòu)的目的:
- 提高可理解性
- 降低修改成本
這兩點(diǎn),無論從可驗(yàn)證性,還是可被度量角度都比較困難。如果項(xiàng)目僅以短期結(jié)果度量,重構(gòu)成果很難自證明。再加之改動(dòng)較大,可能引入一系列不確定因素,無功還有過,自然吃力不討好,所以我們?cè)谶M(jìn)行遺留代碼重構(gòu)時(shí)要充分考慮收益和風(fēng)險(xiǎn),收益盡量考慮可被驗(yàn)證、被度量要素,風(fēng)險(xiǎn)充分考慮成本、時(shí)間、范圍等項(xiàng)目關(guān)注要素。在一個(gè)工程師話語權(quán)不是那么大的公司,這一點(diǎn)尤為重要。
基于上述場景分析,定義了遺留代碼重構(gòu)決策表:
從重構(gòu)帶來的收益和風(fēng)險(xiǎn)兩個(gè)維度,綜合考量、打分,給出一個(gè)簡單、可度量、易被執(zhí)行的決策表。下面我們逐一分析下每條決策項(xiàng):
收益
- 性能瓶頸
看到這條,你一定很不解:一些經(jīng)驗(yàn)也告訴我們,軟件的擴(kuò)展性,常會(huì)犧牲一些性能;再看看《重構(gòu)》書中一段描述:
為了讓軟件易于理解,你常會(huì)做出一些使程序運(yùn)行變慢的修改
而更好的可讀性及好的擴(kuò)展性,恰是重構(gòu)追求的,豈不是自相矛盾?
關(guān)于性能優(yōu)化,我會(huì)在另一篇文章中詳細(xì)闡述,我們先看結(jié)果,重構(gòu)會(huì)給我們帶來如下在性能方面的改善:
- 結(jié)構(gòu)良好的代碼,在性能分析時(shí)有更細(xì)的粒度,更容易發(fā)現(xiàn)性能瓶頸
- 邏輯清晰的軟件,更容易反映軟件業(yè)務(wù)本質(zhì),而清楚我們真正要解決的問題,對(duì)性能往往有意想不到的提升
- 對(duì)軟件結(jié)構(gòu)的調(diào)整,使得對(duì)象及對(duì)象之間的關(guān)系更合理,可以大量減少內(nèi)存浪費(fèi)
- 多核、分布式場景下,性能的瓶頸往往不是計(jì)算本身,而是不合理的調(diào)度,對(duì)軟件結(jié)構(gòu)的調(diào)整,可以從根本上解除該部分約束
另外,性能優(yōu)化很容易被度量,該部分產(chǎn)生的成果很容易被項(xiàng)目接受。
- 高危、高頻故障
看到這條,你又開始不解了,重構(gòu)是“在不改變軟件可觀察行為的前提下”進(jìn)行的,而故障本身就是軟件在特定場景下的錯(cuò)誤行為,所以重構(gòu)是改變不了故障本身的。那對(duì)高危、高頻故障模塊,重構(gòu)的價(jià)值在哪里呢?
- 某模塊故障總是消滅一波,又來一波,攻不死,殺不完,一方面,說明該模塊需求變化還是很頻繁的,另一方面,說明模塊設(shè)計(jì)出了問題,要么是邏輯混亂,要么是內(nèi)部耦合太大,這些都可以通過重構(gòu)來消除。
- 重構(gòu)的一個(gè)目的是“提高可理解性”,邏輯清晰、整潔的代碼,使故障就像白墻上的蒼蠅,很容易發(fā)現(xiàn),解決。
- 重構(gòu)的另一個(gè)目的是“降低修改成本”,軟件容易修改,需要軟件遵循開放封閉原則,修改代碼不影響原有功能,也就避免了增加功能、修改故障引入的新問題。
- 故障數(shù)是一個(gè)容易度量的指標(biāo),效果很容易可視化。
- 新功能擴(kuò)展困難
軟件之所以需要設(shè)計(jì),而不僅僅實(shí)現(xiàn)功能,一方面可以被復(fù)用;另一方面容易增加功能。新增功能困難,并非是無法增加功能,而是,增加功能需要改動(dòng)很多代碼,從而帶來更多風(fēng)險(xiǎn),更大維護(hù)成本。 重構(gòu)通過對(duì)軟件內(nèi)部結(jié)構(gòu)的調(diào)整,不斷消除重復(fù),劃分不同層次,使得新增功能對(duì)原有功能影響盡量小。
- 代碼邏輯混亂,可讀性差
編寫易讀、易理解的代碼,并不像說的那么容易,因?yàn)樗欠粗庇X的,它產(chǎn)生的價(jià)值不是對(duì)當(dāng)下的自己,而是以后的自己或者其他人,需要換位思考。
簡單分享下自己對(duì)編碼認(rèn)識(shí)的幾個(gè)階段:
- 實(shí)現(xiàn)功能,追求性能
- 考慮擴(kuò)展性,增加功能比較容易
- 考慮易理解,維護(hù)代碼比較容易
- 考慮易復(fù)用,除了自己,期望他人也可以用
重構(gòu)對(duì)易理解性帶來的收益:
- 對(duì)代碼重構(gòu)的過程,是對(duì)代碼所表述業(yè)務(wù)邏輯再理解的過程。
- 易理解的代碼,更容易發(fā)現(xiàn)業(yè)務(wù)本質(zhì)
- 人員能力提升
這里的人員能力提升包括兩個(gè)方面:
- 業(yè)務(wù)能力提升。重構(gòu)過程中是對(duì)業(yè)務(wù)邏輯再理解的過程,通過一層層抽絲剝繭,我們也更了解業(yè)務(wù)本身。
- 技術(shù)能力提升。無論是重構(gòu)到Clean Code,還是重構(gòu)到模式,我們的抽象能力、設(shè)計(jì)能力會(huì)伴隨著這個(gè)過程逐漸提升。
風(fēng)險(xiǎn)
任何一件事,當(dāng)我們看到收益的同時(shí),應(yīng)該評(píng)估它帶來的風(fēng)險(xiǎn)。對(duì)于遺留代碼的重構(gòu),在動(dòng)工之前,我們需要回答如下問題:
- 重構(gòu)的主要目標(biāo)是什么?因?yàn)樵谥貥?gòu)過程中,難免會(huì)遇到抉擇和舍棄,如果沒想清楚我們的主要目標(biāo),容易搖擺不定或者迷失了方向。
- 重構(gòu)的范圍是什么?重構(gòu)最容易掉入的一個(gè)陷阱就是,重構(gòu)范圍越來越大,大到無法收手。
- 重構(gòu)的計(jì)劃是什么?雖然重構(gòu)過程中,有太多的不確定因素,極端場景下,重構(gòu)的結(jié)果給當(dāng)初認(rèn)為的完全不一樣,但我們確實(shí)需要一個(gè)時(shí)間盒,在它的約束下,我們更容易集中精力達(dá)成我們預(yù)期的目標(biāo)。
- 重構(gòu)真的必要嗎?有沒有低成本的替代方案?雖然我們鼓勵(lì)用技術(shù)解決問題,但生活中的確存在很多在研發(fā)來看很重要,從商業(yè)角度“然并卵”的事。
想清楚上面的問題后,繼續(xù)考慮如下維度:
- 人員支撐情況
人是重構(gòu)的核心資源,靠譜的人才能做出靠譜的產(chǎn)品。一方面,重構(gòu)的質(zhì)量、完成的速度依賴人,另一方面,重構(gòu)過后代碼的維護(hù)及架構(gòu)的演進(jìn)也依賴人。需靠考慮如下幾個(gè)方面:
- 重構(gòu)要求不能改變軟件的外部行為,我們還期望通過重構(gòu)可以簡化設(shè)計(jì),縮小業(yè)務(wù)與實(shí)現(xiàn)之間的Gap,這就需要有熟悉業(yè)務(wù)人員。你可能會(huì)說:“業(yè)務(wù)全在代碼里了,自己看不就行了”,說的沒錯(cuò),只是太累了
- 嚴(yán)格按照重構(gòu)手法,基本可以做到重構(gòu)前后業(yè)務(wù)邏輯的一致,這就需要至少有人熟悉重構(gòu)技法。
- 高效率來自專注,如果不能全身心投入,或者任務(wù)不斷切換,結(jié)果往往勞力又勞心。
- 團(tuán)隊(duì)中有Tech Lead,不但可以幫助提升團(tuán)隊(duì)重構(gòu)技能,在團(tuán)隊(duì)產(chǎn)生技術(shù)爭執(zhí)時(shí),還可以進(jìn)行裁決。
- QA是團(tuán)隊(duì)交付產(chǎn)品質(zhì)量的最后一道防線,如果重構(gòu)過程中,能不斷得到對(duì)重構(gòu)質(zhì)量的反饋,可以大大降低重構(gòu)帶來的風(fēng)險(xiǎn)。
- 重構(gòu)周期
每個(gè)產(chǎn)品都有版本計(jì)劃及市場使命。如果產(chǎn)品即將退市,對(duì)它進(jìn)行的重構(gòu),無疑是沒有任何意義的,因?yàn)橹貥?gòu)后的軟件已經(jīng)沒有上場表演的機(jī)會(huì)。重構(gòu)需要根據(jù)市場需求和重構(gòu)時(shí)間,選擇能切入的時(shí)機(jī)。比較有效的一個(gè)方法是Small Step重構(gòu),把重構(gòu)任務(wù)進(jìn)行拆解,切分到一個(gè)個(gè)迭代中增量完成。
遙遙無期的重構(gòu),由于項(xiàng)目看不到短期收益,容易動(dòng)搖支持重構(gòu)的決心;另外,在重構(gòu)期間,可能還不斷有新功能加入,為了做到可以替代原有產(chǎn)品,在重構(gòu)同時(shí),還需要不斷追趕這些功能,巨大的壓力,容易使團(tuán)隊(duì)身心疲憊。
- 代碼度量數(shù)據(jù)
平均圈復(fù)雜度、函數(shù)平均行數(shù)、代碼總行數(shù)、重復(fù)度等代碼度量,可以作為是否進(jìn)行重構(gòu)的參考,也是預(yù)估重構(gòu)周期的一個(gè)重要指標(biāo)。另外,重構(gòu)過程中,在CI部署代碼度量檢查,可以看到代碼復(fù)雜度不斷下降,提升堅(jiān)持重構(gòu)的信心。
- 自動(dòng)化測試包圍情況
保證重構(gòu)“不改變軟件可觀察行為”最有效的舉措,就是待重構(gòu)代碼已經(jīng)有大量自動(dòng)化測試用例包圍??紤]如下情況:
- 測試用例最好是基于業(yè)務(wù)進(jìn)行拆分,并且覆蓋場景比較全面
- 測試框架支持不同平臺(tái),可以減少重構(gòu)對(duì)平臺(tái)環(huán)境的依賴,自由選擇
- 如果已有測試用例執(zhí)行速度較快,可以保證重構(gòu)有更好的節(jié)奏感。
如果測試用例覆蓋場景較少,不推薦補(bǔ)充完所有場景測試用例后再進(jìn)行重構(gòu)。一個(gè)推薦的做法是,按照重構(gòu)計(jì)劃,先補(bǔ)充某個(gè)場景用例,然后對(duì)其進(jìn)行重構(gòu),交付后繼續(xù)進(jìn)行下一個(gè)場景,循環(huán)迭代,直到所有場景都完成。
另外,在CI中部署分支覆蓋率監(jiān)控工具,可以感知到分支覆蓋情況逐漸變好,在代碼重構(gòu)完成同時(shí),也交付了一份自動(dòng)化測試用例(當(dāng)然,分支覆蓋率僅能保證分支被跑到,并不能保證邏輯正確)。
遺留代碼重構(gòu)決策表(Excel版)

下載地址:https://github.com/liyongshun/refactor/blob/master/refactor_decision_list.xlsx