代碼重構(gòu)的那些坑和實(shí)戰(zhàn)經(jīng)驗(yàn)
2016年冬,我在一家創(chuàng)業(yè)公司的小團(tuán)隊(duì)里搞軟件開發(fā)。彼時(shí)我們有一位真實(shí)的企業(yè)客戶,且軟件的第一版也已發(fā)布。開發(fā)按進(jìn)度完工,在發(fā)布時(shí)我欣喜若狂,也非常驕傲,看著系統(tǒng)服務(wù)于每天幾百萬的獨(dú)立用戶,并發(fā)送出數(shù)千萬條短信真是太令人滿意了。到了第二年夏天,公司拿到了真實(shí)收入,我的職位變成了開發(fā)主管,公司又招了些新人,正待蓬勃發(fā)展,一切都很美好。然后我們做了一個(gè)巨大的決策失誤:決定重寫軟件——從頭開始。
為什么我們覺得有必要從頭重寫軟件呢?
在第一次編寫系統(tǒng)代碼時(shí),我們的時(shí)間表十分緊迫,必須與時(shí)間賽跑,在計(jì)劃時(shí)間內(nèi)趕完進(jìn)度。因此無論是設(shè)計(jì)討論,還是審查會(huì)議都沒花太長(zhǎng)時(shí)間——我們沒有時(shí)間浪費(fèi)在這上面——只能匆匆完成一個(gè)功能、快速測(cè)試,然后趕著去做下一個(gè)。我們與別的公司共享辦公空間,我還記得其他公司的軟件開發(fā)都會(huì)花很長(zhǎng)時(shí)間做設(shè)計(jì)、討論架構(gòu),再花上數(shù)周討論設(shè)計(jì)模型。
除了設(shè)計(jì)倉(cāng)促,原本的系統(tǒng)寫得不差,總體來說架構(gòu)也不錯(cuò)。其中有些意大利面條式的代碼,是公司之前做概念驗(yàn)證時(shí)留下的,因?yàn)檫@些代碼能用,再加上工期緊張,當(dāng)時(shí)我們沒有去碰。但后來我們不考慮執(zhí)行優(yōu)化改進(jìn),卻決定要從頭重寫代碼的原因在于:
- 老舊代碼很糟糕,很難維護(hù);“單一整體式的java架構(gòu)”對(duì)我們的未來發(fā)展不利,無法支持有6千萬移動(dòng)用戶以及多站點(diǎn)部署的大型運(yùn)行商;我想要嘗試炫酷的新技術(shù),比如Apache Cassandra、虛擬化技術(shù)、二進(jìn)制協(xié)議、SOA等等。
結(jié)果很不幸:我們說服了全公司以及董事會(huì),實(shí)現(xiàn)了愿望。
代碼重寫之旅
正式的開發(fā)時(shí)間是從2016年春天開始的,我們將2017年1月末設(shè)定為發(fā)布時(shí)間。由于計(jì)劃太過龐大,我們需要更多的人,于是在印度聘請(qǐng)了顧問與幾個(gè)遠(yuǎn)程開發(fā)者。但是,我們沒有充分預(yù)期到維護(hù)原本系統(tǒng)、進(jìn)行新的開發(fā)工作與理解客戶需求這些并行起來的工作量。
還記得我在文章最開始說過,我們有一個(gè)真實(shí)客戶么?這位客戶是南美最大的移動(dòng)運(yùn)營(yíng)商之一。在我們開發(fā)的系統(tǒng)投入使用后,他們開始對(duì)變更和新功能提出要求,因此我們只能繼續(xù)更新原來的系統(tǒng)。但是,由于這個(gè)系統(tǒng)將會(huì)被廢棄,在更新時(shí)我們總有些敷衍了事,盡可能找借口拒絕了客戶許多的新功能需求。結(jié)果導(dǎo)致了工期拖延,沒能在原定的deadline完成進(jìn)度。事實(shí)上,我們的進(jìn)度拖延了整整8個(gè)月。
不過我們還是先說說結(jié)果吧:當(dāng)項(xiàng)目終于完工時(shí),新系統(tǒng)看起來非常棒,滿足所有需求。我們做了負(fù)載測(cè)試,結(jié)果顯示新系統(tǒng)能很容易地支持超過1億的用戶,配置集中,查看圖表的UI工具也很美觀,是時(shí)候廢棄舊系統(tǒng),改換新系統(tǒng)了……
但是客戶拒絕了升級(jí)的請(qǐng)求:原本的系統(tǒng)已經(jīng)獲得了廣泛應(yīng)用,他們的用戶已經(jīng)開始依賴舊系統(tǒng)了,他們完全不想冒風(fēng)險(xiǎn)。長(zhǎng)話短說,浪費(fèi)了幾個(gè)月之后我們收效甚微。該項(xiàng)目正式宣告失敗。
何時(shí)需要重寫代碼
Joel Spolsky強(qiáng)烈反對(duì)重寫代碼,他建議大家都不要這樣做。不過我不是特別認(rèn)同:有時(shí)候逐步優(yōu)化與重構(gòu)非常困難,唯一讀懂代碼的方式就是重寫。此外軟件開發(fā)人員喜歡編寫代碼,創(chuàng)造新東西——閱讀別人寫的代碼,嘗試?yán)斫馑麄兊拇a與“思維抽象”會(huì)很無聊。不過,優(yōu)秀的程序員也是優(yōu)秀的維護(hù)者。
如果你想要重寫代碼,一定要出于正確的理由,并有著合適的計(jì)劃。比如:
有時(shí)候在發(fā)布新版很久之后,老舊代碼仍需維護(hù),維護(hù)兩個(gè)版本的代碼需要耗費(fèi)大量工作,在開始重寫前請(qǐng)根據(jù)項(xiàng)目規(guī)模評(píng)估所需的時(shí)間與資源。
想想其他失去的機(jī)會(huì),并比較任務(wù)的優(yōu)先級(jí)。
重寫大型系統(tǒng)比小型系統(tǒng)風(fēng)險(xiǎn)更高,考慮一下能否逐步重寫。我們同時(shí)執(zhí)行了以下幾項(xiàng)工作:切換到新的數(shù)據(jù)庫(kù)、使用“SOA”架構(gòu)、更換為二進(jìn)制協(xié)議,其實(shí)本可以逐步執(zhí)行這些更換。
考慮開發(fā)者的偏見。在開發(fā)者想要學(xué)習(xí)新技術(shù)或新語言的時(shí)候,他們會(huì)想要使用這些來重寫某些代碼。不過我不反對(duì)這樣做,這也是良好環(huán)境與文化的標(biāo)志,但應(yīng)當(dāng)將它與風(fēng)險(xiǎn)和機(jī)遇做比較。
Michael Meadows對(duì)何時(shí)有需要進(jìn)行“大型”重寫有著很好的看法:
技術(shù)上
組件的耦合度很高,無法單獨(dú)對(duì)某個(gè)組件進(jìn)行修改。重新設(shè)計(jì)單個(gè)組件會(huì)導(dǎo)致一連串的變化,不僅會(huì)影響到相鄰的組件,甚至間接影響到所有的組件。技術(shù)堆棧太過復(fù)雜,未來狀態(tài)設(shè)計(jì)需要變更很多的基礎(chǔ)架構(gòu)。出于這個(gè)原因執(zhí)行完全重寫十分必要,逐步重新設(shè)計(jì)在這種情況下沒有優(yōu)勢(shì)。重新設(shè)計(jì)單個(gè)組件無論如何都會(huì)導(dǎo)致對(duì)該組件的重寫,在現(xiàn)有設(shè)計(jì)中沒有可以插入新功能的地方。這種情況下逐步重新設(shè)計(jì)沒有優(yōu)勢(shì)。
政策上
贊助商無法理解逐步重新設(shè)計(jì)需要對(duì)項(xiàng)目進(jìn)行長(zhǎng)期投入。不可避免的是:大多數(shù)公司對(duì)于在逐步重新設(shè)計(jì)上繼續(xù)耗費(fèi)預(yù)算沒有興趣。在完全重寫代碼時(shí),這種現(xiàn)象也很難避免,但贊助商更愿意繼續(xù)投入,因?yàn)樗麄儾幌胗弥氤善返男孪到y(tǒng)與部分過時(shí)的舊系統(tǒng)。系統(tǒng)用戶更習(xí)慣使用“原本的界面”:在這種情況下,政策上不會(huì)允許修改系統(tǒng)的重要部分(前端)。但如果完全從頭開始重寫,則會(huì)繞過這個(gè)問題。用戶還會(huì)堅(jiān)持使用“相同的界面”,但這次你反擊的理由更為充足。要記得:逐步重新設(shè)計(jì)的總成本總是要高于完整重寫代碼,但一般來說對(duì)企業(yè)的影響更小一些。在我看來,如果重寫理由充足,公司又有超級(jí)優(yōu)秀的開發(fā)者,那么就開工吧。
放棄正在開發(fā)的項(xiàng)目很危險(xiǎn):浪費(fèi)大量的時(shí)間和金錢重復(fù)實(shí)現(xiàn)已有功能,同時(shí)還會(huì)放棄實(shí)現(xiàn)新功能的機(jī)會(huì),有可能激怒客戶并導(dǎo)致工作計(jì)劃推遲。如果你正在重寫代碼,那是你的權(quán)力,不過請(qǐng)確保這么做的理由正確,同時(shí)了解風(fēng)險(xiǎn)也做了相關(guān)計(jì)劃。