代碼重構(gòu)的藝術(shù)
原文出自:https://juejin.cn/post/6903054491273625614
什么是重構(gòu)
所謂重構(gòu)是這樣一個過程:在不改變代碼外在行為的前提下,對源代碼做出修改,以改進程序的內(nèi)部結(jié)構(gòu),從而使代碼變得易于理解,可維護和可擴展。本質(zhì)上來說重構(gòu)就是在代碼寫好之后改進它的設(shè)計。
重構(gòu)的目的是什么
首先,重構(gòu)是時刻保證代碼質(zhì)量的一個極其有效的手段,不至于讓代碼腐化到無可救藥的地步。項目在演進,代碼不停地在堆砌。如果沒有人為代碼的質(zhì)量負(fù)責(zé)任,代碼總是會往越來越混亂的方向演進。當(dāng)混亂到一定程度之后,量變引起質(zhì)變,項目的維護成本已經(jīng)高過重新開發(fā)一套新代碼的成本,想要再去重構(gòu),已經(jīng)沒有人能做到了。
? 劣質(zhì)代碼可能會影響后續(xù)優(yōu)化的效率,從而進一步造成代碼劣化;隨著時間的推移,這種效應(yīng)將會導(dǎo)致代碼質(zhì)量大幅下降。破窗效應(yīng) (The Broken Windows Theory) ?
其次,優(yōu)秀的代碼或架構(gòu)不是一開始就能完全設(shè)計好的,就像優(yōu)秀的公司和產(chǎn)品也都是迭代出來的。我們無法100%遇見未來的需求,也沒有足夠的精力、時間、資源為遙遠(yuǎn)的未來買單,所以,隨著系統(tǒng)的演進,重構(gòu)代碼也是不可避免的。
何時需要重構(gòu)
「第一次做某件事時只管去做;第二次做類似的事會產(chǎn)生反感,但無論如何還是可以去做;第三次再做類似的事,你就應(yīng)該重構(gòu).」
添加新功能時重構(gòu)
「種一棵樹最好的時間是十年前,其次是現(xiàn)在。」 重構(gòu)的最佳時機就是在添加新的功能之前。再動手添加新功能之前,我們不妨先考慮一下,如果對現(xiàn)有的代碼結(jié)構(gòu)做些微調(diào),是否會使加入新的功能變的容易的多。
? 如果你要給程序添加一個特性,但發(fā)現(xiàn)代碼因缺乏良好的結(jié)構(gòu)而不易于進行更改,那就先重構(gòu)那個程序,使其比較容易添加該特性,然后再添加該特性 ?
修改問題時重構(gòu)
「掃去窗上的塵埃,才可以看到窗外的美景?!?修改一個問題時,我們需要先理解代碼在做什么,然后才可以著手去修改。這段代碼可能是別人寫的,也可能時自己寫的,但無論如何,當(dāng)你覺得這段代碼邏輯糟糕,需要花費幾分鐘才能明白其中的含義時,你就要想著如何去重構(gòu)才可以使代碼變的更加簡潔直觀
有計劃的對代碼重構(gòu)
「找尋重構(gòu)和開發(fā)進度中適合自己的平衡點」 但有些時候我們在準(zhǔn)備重構(gòu)的時會發(fā)現(xiàn),之前的代碼結(jié)構(gòu)和依賴關(guān)系錯綜復(fù)雜,這時我們就要考慮當(dāng)前是否有足夠時間去很好的處理這些混亂的代碼。盡管重構(gòu)的目的是加快開發(fā)速度,但同時重構(gòu)也會拖慢軟件的開發(fā)進度。如果時間充足,那么當(dāng)下就是進行重構(gòu)最好的時機。當(dāng)魚和熊掌不可兼得的時候,應(yīng)當(dāng)保證軟件的開發(fā)進度不受影響,其次才是進行重構(gòu)。可以先把需要重構(gòu)地方記錄下來,整理出一個計劃,在未來的一段時間內(nèi)解決掉。
Code Review時重構(gòu)
「處明者不見暗中一物,處暗者能見明中區(qū)事?!?Code Review有助于在開發(fā)團隊中傳播知識,也有助于讓較有經(jīng)驗的開發(fā)者把知識傳遞給比較欠缺經(jīng)驗的人。Code Review對于編寫清晰的代碼也很重要,我寫的代碼也許對于我自己來說很清晰,但對于別人來說則不然。Code Review讓更多人有機會提出有用的建議來對代碼進行調(diào)整。三人行,則必有我?guī)煛?/p>
何時不應(yīng)該重構(gòu)
「有所為,有所不為?!?并非所有的糟糕代碼都需要重構(gòu),如果你不需要使用到這段代碼,那么就不必花心思去重構(gòu)它。只有你需要理解其中的工作原理時,對其重構(gòu)才有價值。當(dāng)然如果重寫比重構(gòu)更容易,那么就不需要重構(gòu)了。
如何保證重構(gòu)后程序的正確性
保證代碼正確性最好的方法就是進行「單元測試(Unit Testing)」 。當(dāng)重構(gòu)完成之后,如果新的代碼仍然能通過單元測試,那就說明代碼原有邏輯的正確性未被破壞,原有的外部可見行為未變。
測試驅(qū)動開發(fā)是非常完美的方案。但實際上大部分IT公司的程序由于種種原因并沒有單元測試。這時需要一些工具用來幫助我們快速掃描代碼中的問題。比如可以給代碼增加Lint語法檢查,使用SonarQube對代碼進行質(zhì)量和漏洞掃描,前端同學(xué)還可以使用TypeScript等等。把這些代碼自動掃描工具集成到CI里面,可以大幅度 減少出現(xiàn)bug的情況。目前我所在部門前端組的一系列產(chǎn)品包括項目,已經(jīng)把這些功能集成在CI里面的,每次的代碼更新,都會觸發(fā)掃描代碼的流程,CI失敗就無法將代碼合并到開發(fā)分支上面。
有了上述這些還不夠,在重構(gòu)完成之后,還要把改動部分的功能完整的自測一遍,以保證程序無誤。當(dāng)自測通過之后,就可以請測試同學(xué)來幫忙進行更加完整的測試流程。
? 為什么要進行這么嚴(yán)格的測試流程,因為要保證程序可靠性。如果一件事有可能出錯,那么它一定會出錯。?
需要重構(gòu)的Bad Code
糟糕的命名

整潔代碼最重要的一環(huán) 就是好的名字,所以我們要深思熟慮如何給函數(shù)、模塊、變量和類命名,使它們 能清晰地表明自己的功能和用法。
無意義的注釋

學(xué)會只編寫夠用的注釋,過猶不及,應(yīng)當(dāng)重視質(zhì)量而不是數(shù)量
多層的if語句嵌套

if-else在程序設(shè)計中是不可避免的,作為程序員能做的就是減少嵌套,提升代碼的可閱讀性和質(zhì)量
很酷卻不宜理解代碼

上面這種寫法看起來是不是很酷,但是過一段時間再來看,你還能一眼看出這部分功能是做什么的嗎?在我剛接觸后端,使用python的時候?qū)戇^這樣的代碼,結(jié)果就是在排查問題的時候相當(dāng)頭疼。代碼寫的別人看不懂并不厲害,而是寫的誰都看的懂才是厲害。
? 調(diào)試在一開始就比編寫程序困難一倍。因此,按照定義,如果你的代碼寫得非常巧妙,那么你就沒有足夠的能力來調(diào)試它??铝譂h定律 (Kernighan's Law) ?
不必要的繼承寫法
繼承雖然是面向?qū)ο蟮乃拇筇匦灾唬褂美^承可以解決代碼復(fù)用的問題,但也有其缺點: 繼承層次過深、過于復(fù)雜會影響到代碼的可維護性。如果子類中有方法依賴于父類中的 方法或?qū)傩?,那么?dāng)父類發(fā)生改變時,子類很可能會發(fā)生無法預(yù)知的錯誤。
而組合的方式是把類中所有的接口功能單獨實現(xiàn),然后使用這些單獨的對象組合在一起,完成和目標(biāo)類一致的功能。繼承是用來表示類之間的 is-a 關(guān)系,而組合是一種 has-a 的關(guān)系。使用組合+接口+委托的方式可以代替大多數(shù)的繼承場景。
重構(gòu)代碼的設(shè)計原則
開閉原則 (The Open/Closed Principle)
? 實體應(yīng)開放擴展并關(guān)閉修改。?
實體(可以是類、模塊、函數(shù)等)應(yīng)該能夠使它們的行為易于擴展,但是它們的擴展行為不應(yīng)該被修改。
里氏替換原則 (The Liskov Substitution Principle)
? 可以在不破壞系統(tǒng)的情況下,用子類型替換類型。?
如果組件依賴于類型,那么它應(yīng)該能夠使用該類型的子類型,而不會導(dǎo)致系統(tǒng)失敗或者必須知道該子類型的詳細(xì)信息。
依賴反轉(zhuǎn)原則 (The Dependency Inversion Principle)
? 高級模塊不應(yīng)該依賴于低級實現(xiàn)。?
更高級別的協(xié)調(diào)組件不應(yīng)該知道其依賴項的詳細(xì)信息。
接口隔離原則 (The Interface Segregation Principle)
? 不應(yīng)強制任何客戶端依賴于它不使用的方法。?
組件的消費者不應(yīng)該依賴于它實際上不使用的組件函數(shù)。
單一功能原則 (The Single Responsibility Principle)
? 每個模塊或者類只應(yīng)該有一項功能。?
模塊或者類只應(yīng)該做一件事。實際上,這意味著對程序功能的單個小更改,應(yīng)該只需要更改一個組件。例如,更改密碼驗證復(fù)雜性的方式應(yīng)該只需要更改程序的一部分。
合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle)
? 量的使用合成和聚合,而不是繼承關(guān)系達到復(fù)用的目的。?
合成/聚合復(fù)用原則就是指在一個新的對象里通過關(guān)聯(lián)關(guān)系(包括組合關(guān)系和聚合關(guān)系)來使用一些已有的對象,使之成為新對象的一部分;新對象通過委派調(diào)用已有對象的方法達到復(fù)用已有功能的目的。簡而言之:要盡量使用組合/聚合關(guān)系,少用繼承。
總結(jié)
如果文中有錯誤的地方,還望不吝賜教。寫了這么多,其實是想表達一個觀點:代碼是寫給人看的,所以要做到良好的編程風(fēng)格,方便其他人閱讀,維護。 如果你所在的團隊代碼感覺并不十分優(yōu)雅的話,那就應(yīng)當(dāng)重構(gòu)了。引用前面說到的一句話 「種一棵樹最好的時間是十年前,其次是現(xiàn)在。」
??愛心三連擊 1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入Node進階交流群!「在這里有好多 Node 開發(fā)者,會討論 Node 知識,互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
