爛代碼,升維來看是什么問題導(dǎo)致的呢?

爛代碼是個(gè)有意思的話題,Martin Fowler 的著作《重構(gòu):改善既有代碼的設(shè)計(jì)》里面就專門提到了代碼異味(code smells),特別認(rèn)同他說的:“代碼異味是一種表象,它通常對應(yīng)于系統(tǒng)中更深層次的問題。”
如果你的系統(tǒng)中到處都是爛代碼,就要思考深層次的原因了,需求太奇葩?進(jìn)度太緊?水平不行?沒有設(shè)計(jì)?或者多個(gè)因素一起?
Martin給出的解決方案是重構(gòu),這絕對是很好的建議,只是執(zhí)行起來有難度!
- 我想重構(gòu)代碼,但是我沒有工期怎么辦?
- 我好不容易重構(gòu)完,會(huì)不會(huì)沒多久又老樣子了?
- 我自己沒有能力重構(gòu)!
歸根結(jié)底不光是個(gè)技術(shù)問題,還涉及軟件工程和項(xiàng)目管理。
剛寫代碼的時(shí)候,都是爛代碼,后來見過好代碼,也開始學(xué)著提升代碼質(zhì)量,后來自己代碼質(zhì)量上來了發(fā)現(xiàn)別人寫爛代碼,等到帶團(tuán)隊(duì)了就要去解決團(tuán)隊(duì)中爛代碼。
理想態(tài)是團(tuán)隊(duì)的代碼質(zhì)量不錯(cuò),即使有幾個(gè)剛畢業(yè)的新手程序員也不讓爛代碼混進(jìn)去,這樣產(chǎn)出也高,一個(gè)功能實(shí)現(xiàn)速度是別的團(tuán)隊(duì)的1.5倍到2倍甚至更快。
在解決團(tuán)隊(duì)爛代碼問題上的嘗試。
最先嘗試的是代碼審查,所有更新的代碼都先在分支開發(fā),合并到主干前要代碼審查。順便說一下:對于直接在主干開發(fā),代碼合并后再審查那種效果是會(huì)大打折扣的,事后改起來很困難,而且優(yōu)先級會(huì)很低。
代碼審查嚴(yán)格執(zhí)行的話,效果是很明顯的。但也遇到很多問題:
1. 代碼命名、格式不符合規(guī)范,肉眼很難看的過來怎么辦?
2. PR(Pull Request)太大,要審查的代碼太多,看不過來怎么辦?
3. 急著要上線的功能,代碼質(zhì)量一般甚至很爛怎么辦?
4. 自動(dòng)化測試覆蓋不全怎么辦?
5. 代碼能實(shí)現(xiàn)功能,執(zhí)行也沒問題,但是結(jié)構(gòu)混亂或者沒有遵循最佳實(shí)踐,批準(zhǔn)還是不批準(zhǔn)?
6. 原本的項(xiàng)目就是個(gè)“屎山”代碼,新代碼只能修修補(bǔ)補(bǔ)怎么辦?
代碼審查的一部分工作要工具化自動(dòng)化。
很多代碼審查,靠肉眼是查不過來的,需要靠工具配合:
- 源代碼管理工具不可少,可以清楚看到代碼改動(dòng)的代碼審查工具不可少
- CI(持續(xù)集成)必不可少,每次提交代碼要借助CI運(yùn)行一些自動(dòng)化測試
- lint必不可少,借助lint檢查命名、格式等問題
- 自動(dòng)化測試(單元測試和集成測試)不可少,代碼的更新不能破壞現(xiàn)有功能,新增了功能,相應(yīng)的也要新增自動(dòng)化測試代碼
PR不能太大,大PR要盡可能拆成小PR
越小的PR越好Review,反之太難,盡可能拆小
對于工期緊,急于合并的PR,質(zhì)量差的不要輕易合并,需要后續(xù)改進(jìn)的要有Ticket跟蹤
生產(chǎn)環(huán)境的緊急補(bǔ)丁、deadline將近的新功能,這些PR很難說NO,只能先合并,但大部分時(shí)候就是合并了再無后續(xù),最終一點(diǎn)點(diǎn)污染了整個(gè)代碼庫。
所以總的經(jīng)驗(yàn)就是:質(zhì)量差的,寧可推遲發(fā)布;質(zhì)量沒問題,但是測試不全的或者可以有優(yōu)化空間的,都要求創(chuàng)建一個(gè)或多個(gè)Ticket去跟蹤,并且這些Ticket的優(yōu)先級和其他正式功能的Ticket優(yōu)先級是一樣的,必須在后續(xù)的Sprint里面去盡快完成。
對于稍微復(fù)雜一點(diǎn)的功能,寫代碼之前要先做系統(tǒng)設(shè)計(jì)
在代碼審查時(shí),最初經(jīng)常遇到的一個(gè)問題就是,一個(gè)功能安排下去,開發(fā)很快就實(shí)現(xiàn),結(jié)果你一看代碼,質(zhì)量也不算太差,但是沒有遵循好的實(shí)踐,單個(gè)代碼沒問題,合在一起就很難閱讀和維護(hù)。
要說PR不批準(zhǔn)推翻重寫吧,人家都花了幾天甚至幾周時(shí)間在上面了,說要重寫肯定會(huì)很抵觸,要是小修改也意義不啊。這種情況很難處理。
后來就找到一個(gè)好的解決方案,就是先做設(shè)計(jì),因?yàn)樵O(shè)計(jì)階段,只需要寫簡單的文檔,畫幾張結(jié)構(gòu)圖,通過設(shè)計(jì)Review會(huì)議,一發(fā)現(xiàn)方向不對,馬上就可以調(diào)整,很容易就對齊思路,等到最終實(shí)現(xiàn)代碼,提交PR的時(shí)候,基本上和當(dāng)初討論的設(shè)計(jì)不會(huì)有太大出入,也不會(huì)再出現(xiàn)前面說到的要推翻重寫的情況。
寫代碼之前先做設(shè)計(jì)絕對是磨刀不誤砍柴工的好事,寫設(shè)計(jì)文檔倒逼著開發(fā)人員在實(shí)現(xiàn)前先想清楚,設(shè)計(jì)Review也是一個(gè)很好的機(jī)會(huì)讓團(tuán)隊(duì)其他成員學(xué)習(xí)怎么做設(shè)計(jì),怎么遵循最佳實(shí)踐。
唯一的問題就是要求團(tuán)隊(duì)里面有資深的開發(fā)人員,能在設(shè)計(jì)Review期間發(fā)現(xiàn)問題,指出問題,提出改進(jìn)意見,否則就意義不大。
對于什么樣的功能需要做系統(tǒng)設(shè)計(jì),我們團(tuán)隊(duì)是用T恤尺碼來對應(yīng)任務(wù)大小的,比如S、M、L、XL等,我們的要求是M及以上的都要求做系統(tǒng)設(shè)計(jì),S的可以不做。
強(qiáng)制代碼審查后才能合并,以及強(qiáng)制先設(shè)計(jì)再寫代碼,很好的幫助了我的團(tuán)隊(duì)控制代碼質(zhì)量。
但這還不夠,還經(jīng)常遇到的問題就是:一些舊的屎山項(xiàng)目怎么辦?技術(shù)債務(wù)怎么辦?
老項(xiàng)目的策略是這樣的:
1. 不值得維護(hù)的,隔離起來,保證它能運(yùn)行就夠了,只打補(bǔ)丁,不增加新功能,也不會(huì)再更新
2. 需要長期維護(hù)的,需要經(jīng)常新增功能的,那么就要通過重構(gòu)去改進(jìn)完善。
老項(xiàng)目重構(gòu)的常用策略是:
1. 先補(bǔ)自動(dòng)化測試代碼,尤其是集成測試,這些自動(dòng)化測試保證以后我做任何修改,都不會(huì)出大的問題
2. 逐個(gè)模塊替換,而不是一下子推翻重寫然后遷移。
比如要用NextJS重構(gòu)一個(gè)重要的前端項(xiàng)目,不是先寫一個(gè)新項(xiàng)目,然后整個(gè)替換掉,而是先寫創(chuàng)建一個(gè)組件庫,讓這個(gè)組件庫在新舊項(xiàng)目中可以共用,每次新的組件寫好了,替換老項(xiàng)目中的相應(yīng)模塊,在老項(xiàng)目中充分測試后,再應(yīng)用到新項(xiàng)目,這樣等到老項(xiàng)目的模塊都替換的差不多了,新項(xiàng)目也已經(jīng)充分測試過了,遷移的風(fēng)險(xiǎn)就很小。
再說技術(shù)債務(wù),借債不是壞事,但是債務(wù)是有利息的,最好的還債方式就是像房貸一樣,每個(gè)月定期還一部分,而不是攢最后一起還,那太難了。
首先會(huì)用任務(wù)管理系統(tǒng)跟蹤所有的技術(shù)債務(wù),每個(gè)技術(shù)債務(wù)相關(guān)的任務(wù)都會(huì)創(chuàng)建成ticket,對于復(fù)雜的,會(huì)進(jìn)一步拆分成小的ticket。
然后在每一個(gè)Sprint,我技術(shù)債務(wù)相關(guān)的任務(wù)在20%左右,這樣的任務(wù)和產(chǎn)品需求任務(wù)優(yōu)先級是一樣的。這樣基本上我的代碼庫的技術(shù)債務(wù)一直維持在一個(gè)很良性的水平。
有的團(tuán)隊(duì)就是另一種情況,去年花了一個(gè)季度的時(shí)間去償還技術(shù)債務(wù),因?yàn)榈搅藢?shí)在非還不可的地步,那個(gè)季度基本上無法響應(yīng)來自產(chǎn)品的需求。我個(gè)人是不太推薦這種方式的,對開發(fā)團(tuán)隊(duì)和產(chǎn)品團(tuán)隊(duì)都是巨大的壓力。
最近還探索出一個(gè)比較好的開發(fā)模式:
就是兩周的Sprint中,我們第一周只做產(chǎn)品的需求,然后第一周開發(fā)完成后部署到測試環(huán)境測試,第二周就是修復(fù)各種新功能的Bug,但是相應(yīng)的bug修復(fù)工作量沒多少,所以我們在第二周就可以有時(shí)間去做純技術(shù)相關(guān)的任務(wù),比如償還技術(shù)債務(wù)、測試新的技術(shù)棧、開發(fā)公共組件。因?yàn)槌掷m(xù)有產(chǎn)品功能交付,所以產(chǎn)品經(jīng)理也接受了這種模式。
除了老項(xiàng)目的債務(wù)問題,新項(xiàng)目也有一個(gè)問題,就是架構(gòu)設(shè)計(jì)出來了,代碼實(shí)現(xiàn)的時(shí)候,程序員的水平是參差不齊的,有新手有老手,就是是老手,還有各自的個(gè)人喜好在里面,這也給代碼質(zhì)量帶來很大挑戰(zhàn)。
所以如果是我自己設(shè)計(jì)的架構(gòu)或者我?guī)У捻?xiàng)目,我會(huì)在架構(gòu)設(shè)計(jì)完成后,自己或者要求其他人基于架構(gòu)設(shè)計(jì),先實(shí)現(xiàn)一些基本的功能模塊,把基本的場景都覆蓋,在這些模塊實(shí)現(xiàn)的時(shí)候,形成一個(gè)好的開發(fā)實(shí)踐,比如前端項(xiàng)目,可以遵守Redux的狀態(tài)管理實(shí)踐。這個(gè)階段也一樣要有代碼審查,團(tuán)隊(duì)里每個(gè)人都可以提出自己的意見反饋,根據(jù)反饋還可以對最佳實(shí)踐做出調(diào)整。
有了最佳實(shí)踐,其他人在實(shí)現(xiàn)時(shí),就可以照葫蘆畫瓢,按照最佳實(shí)踐就可以寫出質(zhì)量不錯(cuò)的代碼,對于不符合最佳實(shí)踐的代碼,在代碼審查階段就應(yīng)該果斷拒絕。
簡單總結(jié)一下:
1.代碼審查很重要,而且一定要先審查再合并
2.系統(tǒng)設(shè)計(jì)也很重要,堅(jiān)持先系統(tǒng)設(shè)計(jì)再對3.設(shè)計(jì)評審再實(shí)現(xiàn)代碼
4.代碼要有自動(dòng)化測試覆蓋
5.技術(shù)債務(wù)要定期還
6.老項(xiàng)目重構(gòu)可以一點(diǎn)點(diǎn)替換
7.新項(xiàng)目或已有項(xiàng)目要有最佳實(shí)踐供新手參考,并且大家一起遵守
所以爛代碼這個(gè)問題,升維來看本質(zhì)上需求管理問題,需求本身影響著實(shí)現(xiàn)質(zhì)量。缺乏專業(yè)的需求管理能力,盲目擴(kuò)大需求范圍,力求多和快,忘記了好和省了。
