背景
《重構(gòu)》誕生至今有近17個年頭了,日常開發(fā)中大家談到重構(gòu),要么非常隨意,認(rèn)為重構(gòu)就是改代碼;要么非常謹(jǐn)慎,把重構(gòu)描述成焦油坑,像瘟神一樣敬而遠(yuǎn)之。針對最具挑戰(zhàn)性的遺留代碼重構(gòu),有哪些需要注意的呢?
談?wù)撊魏问虑?,都該有它的上下文。本文談?wù)摰募夹g(shù)背景是大型通信類產(chǎn)品,對于互聯(lián)網(wǎng)產(chǎn)品不一定適用。另外,本文也不會涉及重構(gòu)技術(shù),有興趣讀者可以讀《重構(gòu)》或者《Effective Refactoring in C++》。
遺留代碼重構(gòu)決策表
遺留代碼的重構(gòu)屬于《重構(gòu)與收拾屋子》提到的“大掃除”或“裝修”場景。對遺留代碼進行重構(gòu),很容易形成“吃力不討好”的局面,究其原因,我們先回顧下重構(gòu)的目的:
- 提高可理解性
- 降低修改成本
這兩點,無論從可驗證性,還是可被度量角度都比較困難。如果項目僅以短期結(jié)果度量,重構(gòu)成果很難自證明。再加之改動較大,可能引入一系列不確定因素,無功還有過,自然吃力不討好。所以我們在進行遺留代碼重構(gòu)時要充分考慮收益和風(fēng)險,收益盡量考慮可被驗證、被度量要素,風(fēng)險充分考慮成本、時間、范圍等項目關(guān)注要素。在一個工程師話語權(quán)不是那么大的公司,這一點尤為重要。
基于上述場景分析,定義了遺留代碼重構(gòu)決策表:
從重構(gòu)帶來的收益和風(fēng)險兩個維度,綜合考量、打分,給出一個簡單、可度量、易被執(zhí)行的決策表。下面我們逐一分析下每條決策項:
收益
- 性能瓶頸
看到這條,你一定很不解:一些經(jīng)驗也告訴我們,軟件的擴展性,常會犧牲一些性能;再看看《重構(gòu)》書中一段描述:
為了讓軟件易于理解,你常會做出一些使程序運行變慢的修改
而更好的可讀性及好的擴展性,恰是重構(gòu)追求的,豈不是自相矛盾?
關(guān)于性能優(yōu)化,我會在另一篇文章中詳細(xì)闡述,我們先看結(jié)果,重構(gòu)會給我們帶來如下在性能方面的改善:
- 結(jié)構(gòu)良好的代碼,在性能分析時有更細(xì)的粒度,更容易發(fā)現(xiàn)性能瓶頸
- 邏輯清晰的軟件,更容易反映軟件業(yè)務(wù)本質(zhì),而清楚我們真正要解決的問題,對性能往往有意想不到的提升
- 對軟件結(jié)構(gòu)的調(diào)整,使得對象及對象之間的關(guān)系更合理,可以大量減少內(nèi)存浪費
- 多核、分布式場景下,性能的瓶頸往往不是計算本身,而是不合理的調(diào)度,對軟件結(jié)構(gòu)的調(diào)整,可以從根本上解除該部分約束
另外,性能優(yōu)化成果很容易被度量。
- 高危、高頻故障
看到這條,你又開始不解了,重構(gòu)是“在不改變軟件可觀察行為的前提下”進行的,而故障本身就是軟件在特定場景下的錯誤行為,所以重構(gòu)是改變不了故障本身的。那對高危、高頻故障模塊,重構(gòu)的價值在哪里呢?
- 某模塊故障總是消滅一波,又來一波,攻不死,殺不完,一方面,說明該模塊需求變化還是很頻繁的,另一方面,說明模塊設(shè)計出了問題,要么是邏輯混亂,要么是內(nèi)部耦合太大,這些都可以通過重構(gòu)來消除。
- 重構(gòu)的一個目的是“提高可理解性”,邏輯清晰、整潔的代碼,使故障就像白墻上的蒼蠅,很容易發(fā)現(xiàn),解決。
- 重構(gòu)的另一個目的是“降低修改成本”,軟件容易修改,需要軟件遵循開放封閉原則,修改代碼不影響原有功能,也就避免了增加功能、修改故障引入的新問題。
- 故障數(shù)是一個容易度量的指標(biāo),效果很容易可視化。
- 新功能擴展困難
軟件之所以需要設(shè)計,而不僅僅實現(xiàn)功能,一方面可以被復(fù)用;另一方面容易增加功能。新增功能困難,并非是無法增加功能,而是,增加功能需要改動很多代碼,從而帶來更多風(fēng)險,更大維護成本。
重構(gòu)通過對軟件內(nèi)部結(jié)構(gòu)的調(diào)整,不斷消除重復(fù),局部化影響,使得新增功能對原有功能影響盡量小。
- 代碼邏輯混亂,可讀性差
編寫易讀、易理解的代碼,并不像說的那么容易,因為它是反直覺的,它產(chǎn)生的價值不是對當(dāng)下的自己,而是以后的自己或者其他人,需要換位思考。
簡單分享下自己對編碼認(rèn)識的幾個階段:
- 實現(xiàn)功能,追求性能
- 考慮擴展性,增加功能比較容易
- 考慮易理解,維護代碼比較容易
- 考慮易復(fù)用,除了自己,期望他人也可以用
重構(gòu)對代碼易理解性帶來的收益:
- 對代碼重構(gòu)的過程,是對代碼所表述業(yè)務(wù)邏輯再理解的過程。
- 易理解的代碼,更容易發(fā)現(xiàn)業(yè)務(wù)本質(zhì)
- 人員能力提升
這里的人員能力提升包括兩個方面:
- 業(yè)務(wù)能力提升。重構(gòu)過程中是對業(yè)務(wù)邏輯再理解的過程,通過一層層抽絲剝繭,我們也更了解業(yè)務(wù)本身。
- 技術(shù)能力提升。無論是重構(gòu)到Clean Code,還是重構(gòu)到模式,我們的抽象能力、設(shè)計能力會伴隨著這個過程逐漸提升。
風(fēng)險
任何一件事,當(dāng)我們看到收益的同時,應(yīng)該評估它帶來的風(fēng)險。對于遺留代碼的重構(gòu),在動工之前,我們需要回答如下問題:
- 重構(gòu)的主要目標(biāo)是什么?因為在重構(gòu)過程中,難免會遇到抉擇和舍棄,如果沒想清楚我們的主要目標(biāo),容易搖擺不定或者迷失了方向。
- 重構(gòu)的范圍是什么?重構(gòu)最容易掉入的一個陷阱就是,重構(gòu)范圍越來越大,大到無法收手。
- 重構(gòu)的計劃是什么?雖然重構(gòu)過程中,有太多的不確定因素,極端場景下,重構(gòu)的結(jié)果給當(dāng)初認(rèn)為的完全不一樣,但我們確實需要一個時間盒,在它的約束下,我們更容易集中精力達(dá)成我們預(yù)期的目標(biāo)。
- 重構(gòu)真的必要嗎?有沒有低成本的替代方案?雖然我們鼓勵用技術(shù)解決問題,但生活中的確存在很多在研發(fā)來看很重要,從商業(yè)角度“然并卵”的事。
想清楚上面的問題后,繼續(xù)考慮如下維度:
- 人員支撐情況
人是重構(gòu)的核心資源,靠譜的人才能做出靠譜的產(chǎn)品。一方面,重構(gòu)的質(zhì)量、完成的速度依賴人,另一方面,重構(gòu)過后代碼的維護及架構(gòu)的演進也依賴人。需考慮如下幾個方面:
- 重構(gòu)要求不能改變軟件的外部行為,我們還期望通過重構(gòu)可以簡化設(shè)計,縮小業(yè)務(wù)與實現(xiàn)之間的Gap,這就需要有熟悉業(yè)務(wù)人員。你可能會說:“業(yè)務(wù)全在代碼里了,自己看不就行了”,說的沒錯,只是太累了
- 嚴(yán)格按照重構(gòu)手法,基本可以做到重構(gòu)前后業(yè)務(wù)邏輯的一致,這就需要至少有人熟悉重構(gòu)技法。
- 高效率來自專注,如果不能全身心投入,或者任務(wù)不斷切換,結(jié)果往往勞力又勞心。
- 團隊中有Tech Lead,不但可以幫助提升團隊重構(gòu)技能,在團隊產(chǎn)生技術(shù)爭執(zhí)時,還可以進行裁決。
- QA是團隊交付產(chǎn)品質(zhì)量的最后一道防線,如果重構(gòu)過程中,能不斷得到對重構(gòu)質(zhì)量的反饋,可以大大降低重構(gòu)帶來的風(fēng)險。
- 重構(gòu)周期
每個產(chǎn)品都有版本計劃及市場使命。如果產(chǎn)品即將退市,對它進行的重構(gòu),無疑是沒有任何意義的,因為重構(gòu)后的軟件已經(jīng)沒有上場表演的機會。重構(gòu)需要根據(jù)市場需求和重構(gòu)時間,選擇能切入的時機。比較有效的一個方法是Small Step重構(gòu),把重構(gòu)任務(wù)進行拆解,切分到一個個迭代中增量完成。
遙遙無期的重構(gòu),由于項目看不到短期收益,容易動搖支持重構(gòu)的決心;另外,在重構(gòu)期間,可能還不斷有新功能加入,為了做到可以替代原有產(chǎn)品,在重構(gòu)同時,還需要不斷追趕這些功能,巨大的壓力,容易使團隊身心疲憊。
- 代碼度量數(shù)據(jù)
平均圈復(fù)雜度、函數(shù)平均行數(shù)、代碼總行數(shù)、重復(fù)度等代碼度量,可以作為是否進行重構(gòu)的參考,也是預(yù)估重構(gòu)周期的一個重要指標(biāo)。另外,重構(gòu)過程中,在CI部署代碼度量檢查,可以看到代碼復(fù)雜度不斷下降,提升堅持重構(gòu)的信心。
- 自動化測試包圍情況
保證重構(gòu)“不改變軟件可觀察行為”最有效的舉措,就是待重構(gòu)代碼已經(jīng)有大量自動化測試用例包圍??紤]如下情況:
- 測試用例最好是基于業(yè)務(wù)進行拆分,并且覆蓋場景比較全面
- 測試框架支持不同平臺,可以減少重構(gòu)對平臺環(huán)境的依賴,自由選擇
- 如果已有測試用例執(zhí)行速度較快,可以保證重構(gòu)有更好的節(jié)奏感。
如果測試用例覆蓋場景較少,不推薦補充完所有場景測試用例后再進行重構(gòu)。一個推薦的做法是,按照重構(gòu)計劃,先補充某個場景用例,然后對其進行重構(gòu),交付后繼續(xù)進行下一個場景,循環(huán)迭代,直到所有場景都完成。
另外,在CI中部署分支覆蓋率監(jiān)控工具,可以感知到分支覆蓋情況逐漸變好,在代碼重構(gòu)完成同時,也交付了一份自動化測試用例(當(dāng)然,分支覆蓋率僅能保證分支被跑到,并不能保證邏輯正確)。
遺留代碼重構(gòu)決策表(Excel版)
下載地址:
https://github.com/liyongshun/refactor/blob/master/refactor_decision_list.xlsx