淺談遺留代碼的重構

背景

《重構》誕生至今有近17個年頭了,日常開發中大家談到重構,要麼非常隨意,認爲重構就是改代碼;要麼非常謹慎,把重構描述成焦油坑,像瘟神一樣敬而遠之。針對最具挑戰性的遺留代碼重構,有哪些需要注意的呢?

談論任何事情,都該有它的上下文。本文談論的技術背景是大型通信類產品,對於互聯網產品不一定適用。另外,本文也不會涉及重構技術,有興趣讀者可以讀《重構》或者《Effective Refactoring in C++》。

遺留代碼重構決策表

遺留代碼的重構屬於《重構與收拾屋子》提到的“大掃除”或“裝修”場景。對遺留代碼進行重構,很容易形成“吃力不討好”的局面,究其原因,我們先回顧下重構的目的:

  • 提高可理解性
  • 降低修改成本

這兩點,無論從可驗證性,還是可被度量角度都比較困難。如果項目僅以短期結果度量,重構成果很難自證明。再加之改動較大,可能引入一系列不確定因素,無功還有過,自然吃力不討好。所以我們在進行遺留代碼重構時要充分考慮收益和風險,收益儘量考慮可被驗證、被度量要素,風險充分考慮成本、時間、範圍等項目關注要素。在一個工程師話語權不是那麼大的公司,這一點尤爲重要。

基於上述場景分析,定義了遺留代碼重構決策表:

從重構帶來的收益和風險兩個維度,綜合考量、打分,給出一個簡單、可度量、易被執行的決策表。下面我們逐一分析下每條決策項:

收益

  1. 性能瓶頸

看到這條,你一定很不解:一些經驗也告訴我們,軟件的擴展性,常會犧牲一些性能;再看看《重構》書中一段描述:

爲了讓軟件易於理解,你常會做出一些使程序運行變慢的修改

而更好的可讀性及好的擴展性,恰是重構追求的,豈不是自相矛盾?

關於性能優化,我會在另一篇文章中詳細闡述,我們先看結果,重構會給我們帶來如下在性能方面的改善:

  • 結構良好的代碼,在性能分析時有更細的粒度,更容易發現性能瓶頸
  • 邏輯清晰的軟件,更容易反映軟件業務本質,而清楚我們真正要解決的問題,對性能往往有意想不到的提升
  • 對軟件結構的調整,使得對象及對象之間的關係更合理,可以大量減少內存浪費
  • 多核、分佈式場景下,性能的瓶頸往往不是計算本身,而是不合理的調度,對軟件結構的調整,可以從根本上解除該部分約束

另外,性能優化成果很容易被度量。

  1. 高危、高頻故障

看到這條,你又開始不解了,重構是“在不改變軟件可觀察行爲的前提下”進行的,而故障本身就是軟件在特定場景下的錯誤行爲,所以重構是改變不了故障本身的。那對高危、高頻故障模塊,重構的價值在哪裏呢?

  • 某模塊故障總是消滅一波,又來一波,攻不死,殺不完,一方面,說明該模塊需求變化還是很頻繁的,另一方面,說明模塊設計出了問題,要麼是邏輯混亂,要麼是內部耦合太大,這些都可以通過重構來消除。
  • 重構的一個目的是“提高可理解性”,邏輯清晰、整潔的代碼,使故障就像白牆上的蒼蠅,很容易發現,解決。
  • 重構的另一個目的是“降低修改成本”,軟件容易修改,需要軟件遵循開放封閉原則,修改代碼不影響原有功能,也就避免了增加功能、修改故障引入的新問題。
  • 故障數是一個容易度量的指標,效果很容易可視化。
  1. 新功能擴展困難

軟件之所以需要設計,而不僅僅實現功能,一方面可以被複用;另一方面容易增加功能。新增功能困難,並非是無法增加功能,而是,增加功能需要改動很多代碼,從而帶來更多風險,更大維護成本。

重構通過對軟件內部結構的調整,不斷消除重複,局部化影響,使得新增功能對原有功能影響儘量小。

  1. 代碼邏輯混亂,可讀性差

編寫易讀、易理解的代碼,並不像說的那麼容易,因爲它是反直覺的,它產生的價值不是對當下的自己,而是以後的自己或者其他人,需要換位思考。

簡單分享下自己對編碼認識的幾個階段:

  1. 實現功能,追求性能
  2. 考慮擴展性,增加功能比較容易
  3. 考慮易理解,維護代碼比較容易
  4. 考慮易複用,除了自己,期望他人也可以用

重構對代碼易理解性帶來的收益:

  • 對代碼重構的過程,是對代碼所表述業務邏輯再理解的過程。
  • 易理解的代碼,更容易發現業務本質
  1. 人員能力提升

這裏的人員能力提升包括兩個方面:

  1. 業務能力提升。重構過程中是對業務邏輯再理解的過程,通過一層層抽絲剝繭,我們也更瞭解業務本身。
  2. 技術能力提升。無論是重構到Clean Code,還是重構到模式,我們的抽象能力、設計能力會伴隨着這個過程逐漸提升。

風險

任何一件事,當我們看到收益的同時,應該評估它帶來的風險。對於遺留代碼的重構,在動工之前,我們需要回答如下問題:

  • 重構的主要目標是什麼?因爲在重構過程中,難免會遇到抉擇和捨棄,如果沒想清楚我們的主要目標,容易搖擺不定或者迷失了方向。
  • 重構的範圍是什麼?重構最容易掉入的一個陷阱就是,重構範圍越來越大,大到無法收手。
  • 重構的計劃是什麼?雖然重構過程中,有太多的不確定因素,極端場景下,重構的結果給當初認爲的完全不一樣,但我們確實需要一個時間盒,在它的約束下,我們更容易集中精力達成我們預期的目標。
  • 重構真的必要嗎?有沒有低成本的替代方案?雖然我們鼓勵用技術解決問題,但生活中的確存在很多在研發來看很重要,從商業角度“然並卵”的事。

想清楚上面的問題後,繼續考慮如下維度:

  1. 人員支撐情況

人是重構的核心資源,靠譜的人才能做出靠譜的產品。一方面,重構的質量、完成的速度依賴人,另一方面,重構過後代碼的維護及架構的演進也依賴人。需考慮如下幾個方面:

  • 重構要求不能改變軟件的外部行爲,我們還期望通過重構可以簡化設計,縮小業務與實現之間的Gap,這就需要有熟悉業務人員。你可能會說:“業務全在代碼裏了,自己看不就行了”,說的沒錯,只是太累了
  • 嚴格按照重構手法,基本可以做到重構前後業務邏輯的一致,這就需要至少有人熟悉重構技法。
  • 高效率來自專注,如果不能全身心投入,或者任務不斷切換,結果往往勞力又勞心。
  • 團隊中有Tech Lead,不但可以幫助提升團隊重構技能,在團隊產生技術爭執時,還可以進行裁決。
  • QA是團隊交付產品質量的最後一道防線,如果重構過程中,能不斷得到對重構質量的反饋,可以大大降低重構帶來的風險。
  1. 重構週期

每個產品都有版本計劃及市場使命。如果產品即將退市,對它進行的重構,無疑是沒有任何意義的,因爲重構後的軟件已經沒有上場表演的機會。重構需要根據市場需求和重構時間,選擇能切入的時機。比較有效的一個方法是Small Step重構,把重構任務進行拆解,切分到一個個迭代中增量完成。

遙遙無期的重構,由於項目看不到短期收益,容易動搖支持重構的決心;另外,在重構期間,可能還不斷有新功能加入,爲了做到可以替代原有產品,在重構同時,還需要不斷追趕這些功能,巨大的壓力,容易使團隊身心疲憊。

  1. 代碼度量數據

平均圈複雜度、函數平均行數、代碼總行數、重複度等代碼度量,可以作爲是否進行重構的參考,也是預估重構週期的一個重要指標。另外,重構過程中,在CI部署代碼度量檢查,可以看到代碼複雜度不斷下降,提升堅持重構的信心。

  1. 自動化測試包圍情況

保證重構“不改變軟件可觀察行爲”最有效的舉措,就是待重構代碼已經有大量自動化測試用例包圍。考慮如下情況:

  • 測試用例最好是基於業務進行拆分,並且覆蓋場景比較全面
  • 測試框架支持不同平臺,可以減少重構對平臺環境的依賴,自由選擇
  • 如果已有測試用例執行速度較快,可以保證重構有更好的節奏感。

如果測試用例覆蓋場景較少,不推薦補充完所有場景測試用例後再進行重構。一個推薦的做法是,按照重構計劃,先補充某個場景用例,然後對其進行重構,交付後繼續進行下一個場景,循環迭代,直到所有場景都完成。

另外,在CI中部署分支覆蓋率監控工具,可以感知到分支覆蓋情況逐漸變好,在代碼重構完成同時,也交付了一份自動化測試用例(當然,分支覆蓋率僅能保證分支被跑到,並不能保證邏輯正確)。

遺留代碼重構決策表(Excel版)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章