好的重构方法才能摆脱“屎山”
大家好,我是Z哥。
最近在整理一些項目,所以相關的文章寫的多了些。之前的相關文章有《聊聊單元測試》,感興趣的話可以點擊文末鏈接去閱讀。
這次整理項目的時候,做了比較多的codereview和重構。好久沒做這么高強度了重構了,所以對重構這件事有了新的思考和理解。
突然發現叫我們程序員“碼農”還挺形象的,因為寫代碼和種田很像,想有個好收成,就要好好管理代碼,讓它們井井有條。
吳軍老師在《文明之光》里講到一個「壟耕種植法」,它由中國人發明,后發揚到全球,影響了全世界的糧食生產。據說歐洲人民以前是把種子隨意地撒在地里,任其自由生長,結果收成很低,如果種下20斤,大概只能收獲60斤左右糧食。而中國早在先秦時期畝產最少都在240斤以上,最新的數據是今年11月初袁隆平的雜交水稻,早晚稻加起來達到3000斤,這都得益于「壟耕種植法」。
所以,當你看到那些被隨意“播種”的糟糕代碼,是改,還是不改?改吧,花時間;不改吧,就像上面的歐洲人民。
其實很多人對「重構」的理解還有些誤區。「重構」僅僅是所謂的優化代碼嗎?并不是。
Martin Fowler大神在他的《重構》一書中對「重構」的定義就非常準確。
重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。?
重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。
《重構》Martin Fowler
所以,重構不僅僅是修改代碼,是對軟件結構的調整,修改代碼只是其中的一個手段而已。
那么具體應該怎么做呢?在這之前,需要考慮清楚以下幾個問題。
/01 ?什么時候重構?/
很多理想主義者認為的理想情況自然是隨時發現壞代碼就重構。但是這里存在兩個問題:
有很多客觀因素導致了就算發現了壞代碼,也不一定有充足的時間來修改。
不同的人代碼水平不一,一個人看起來沒毛病的代碼,可能在其他人眼里卻是需要重構的代碼。
所以我們需要幾個更加客觀的外部標準,Z哥建議你可以從以下三個方面來觀察,如果發現了類似的現象,說明它在給你發出需要重構的信號。
增加新功能的時候。此時,你發現既有的代碼無法讓你輕松添加新特性,需要花30%以上的時間做一些重復性的編碼。
修bug的時候。此時,你發現排查邏輯非常困難,需要花費半小時甚至是數小時才能搞清楚代碼在處理什么。
當從優秀的程序員口中說出覺得某段的代碼寫的太shit。(普通人的shit可能是到了無可救藥的地步了,優秀的人覺得shit大概率還有救,否則他估計就離職不干了~)
/02 ?怎么重構?/
為了保證重構的質量,在你重構的過程中,一定要關注以下4個關鍵點:
始終工作。這其實也是《重構》中提到的理念,“「不改變軟件可觀察行為」的前提下”。如果你的重構需要大刀闊斧的進行,會導致很長一段時間軟件無法運行起來,那么這就不是一個好的重構方式,因為在很長一段時間里你都不知道它會不會走向萬劫不復的“深淵”。(有沒有遇到過越重構越糟糕的經歷?歡迎分享你的吐槽)
持續集成:很多人會將重構單獨作為一個版本去做,其實這樣會有一個新的問題,就是后續合版本是個痛苦的事情,而且容易出錯。所以,應該就在平時的版本里去做重構,讓每一次重構都可以跟著CI/CD持續進行。
隨時中止:不管你的重構代碼寫到什么階段,只要編譯不報錯就可以隨時中止,立馬切換到功能開發。只有達到這個標準,你的Leader才不會對重構又愛又恨。
過程可逆:假如我的重構出現了重大缺陷,它是可以快速回退的,而不需要從之前的版本當中去撈代碼。畢竟,誰都無法100%保證每一次的重構都是perfect的。
可以回想一下,你之前做過的重構是否都符合了以上的這些要求?反正Z哥最近做的重構是不符合的,所以感覺很累很痛苦~
具體的重構工作其實說起來很簡單,因為一段代碼無非就是「輸入參數」、「輸出參數」、「方法體」3個東西,重構也自然以這幾個地方展開。
/01 ?輸入參數/
對于輸入參數的重構,主要關注在參數的個數上。那些優秀的開源項目里,你幾乎看不到參數很多的方法。
因為過多的參數個數,不但不容易理解,而且你在寫調用這個方法的代碼的時候也會很頭疼,時不時要數一下這是第幾個參數,對應的參數說明是什么。
有一些工具推薦的默認參數最大長度是7個(如SonarQube)。如果你沒有更好的定義和理解,那么不妨以“7”這個標準來執行。
/02? 輸出參數/
輸出參數只有一個,能夠出亂子的空間也很小,所以一般來說不需要怎么優化。
唯一值得提醒的兩點是:
參數類型盡量用強類型。弱類型的返回值雖然讓你的Function向后兼容性很好,但是也帶來了很多無法在編譯期間被發現的問題。
不返回不需要的參數。添加更多參數在最初肯定是為了“跑在業務前面”,但這份好心往往最終帶來的是更多“意料之外的耦合”,導致后續的重構成本大增。
/03? 方法體/
對于方法體的重構是花費時間最多的地方,具體的方式方法也很多。但是我建議你一定要堅持一個核心要點,我將它稱為「NRD重構法」,這3個字母分別表示:New、Replace、Delete。也就是說,做重構的時候不要直接在原來的方法體里改,重新建一個新的方法,然后等單測跑通之后再替換掉老方法,最后再把老方法刪除。
只要做到這點,要滿足前面提到的4個關鍵點,就沒那么困難了。
具體的重構內容自然是以減少復雜度為核心思路去做。衡量代碼復雜度有一個概念叫「圈復雜度」(也叫「循環復雜度」),在1976年由Thomas J. McCabe, Sr. 提出。現在有不少工具有統計這個指標。
復雜度大說明程序代碼可能質量低且難于測試和維護,根據經驗,程序的可能錯誤和高的圈復雜度有著很大關系。
復雜度大的代碼往往伴隨著大量的if/switch/for/foreach/try...catch/while等等。每一次試用都會讓「圈復雜度」+1,并且其中的條件判斷越多,增加的越快。
所以,常見的重構方式大多以降低代碼的圈復雜度為主。比如,
將相同邏輯的代碼抽離并封裝到一處,可以避免在多個方法體里增加圈復雜度,只在一個方法體里增加。
通過AOP技術,不但可以將重復的代碼剔除出當前方法體,還可以將try...catch之類的代碼剔除出去,以降低復雜度。
通過一些語法糖或者框架,也可以降低復雜度。如,lambda表達式。
……
還有很多小眾的重構技巧這里就不贅述了,真是覺得大家都應該讀一讀《重構》這本書。
多說一句,不提倡刻意降低代碼行數的方法,因為你的復雜度不下降,減少代碼行數只是“掩耳盜鈴”而已。
另外,重構有一個最佳伴侶,就是單元測試。你想象一個畫面,當你重構之前通過率100%的單元測試在重構完成后跑一遍,發現了10%的失敗。此時你的心情肯定是“真香,否則一堆bug等著我修”。
不過,如果你的代碼「圈復雜度」越高,單元測試寫起來越費勁。如何寫好單元測試可以看我之前寫的文章《聊聊單元測試》。
最后,怎么判斷重構的效果好不好呢?自然是工作效率是否提高了。
增加一個功能或者接口的時間是不是縮短了?
測試那邊回歸測試的平均時間是不是縮短了?
……
好了,就這么多。如果你還是覺得無從下手,不妨試試《重構》作者推薦的一種做法:
隨機挑選一個目標,比如,“去掉一堆不必要的子類”。然后朝著目標前進,沒把握就停下來。當你無法證明自己所做的修改能夠保證原有程序的邏輯和語義時,立馬停下來思考:當前做的重構是改善了?還是毫無成果需要撤銷?
最后再次強力推薦《重構》這本書,里面有很多非常具體的代碼重構方法,值得每一位程序員入手一本。
好了,總結一下。
這篇呢,Z哥和你分享了我對代碼重構這件事的看法。要想提高你代碼的“產出”,那么就得好好重視重構這件事。
在重構代碼的「輸入參數」、「輸出參數」、「方法體」的時候需要持續保持以下4個關鍵點:
始終工作
持續集成
隨時中止
過程可逆
這才能使得你的重構工作平穩的進行,而不會是一場賭博。
并且,重構方法體的時候要以降低「圈復雜度」為目的,而不是代碼行數。如果條件允許,盡量多寫一些單元測試來保障重構的穩定性。
希望對你有所啟發。
重構可以使軟件更容易地被修改和被理解,這個意義甚至大于所謂的“優化和改進”。Kent Beck大神曾也經說過:首先讓代碼架構易于改變,然后再進行簡單的改進。
如果你想擺脫代碼越改越痛苦的困境,那么趕緊行動起來吧。
推薦閱讀:
做架構也得講武德
聊聊單元測試
原創不易,如果你覺得這篇文章還不錯,就「在看」或者「分享」一下吧。鼓勵我的創作 :)
如果你有關于軟件架構、分布式系統、產品、運營的困惑
可以試試點擊「閱讀原文」
總結
以上是生活随笔為你收集整理的好的重构方法才能摆脱“屎山”的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务技术栈及分享计划
- 下一篇: VS Code 变身约会利器!以码会友,