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