重构,还是重写?(2020版)
Joel Spolsky (軟件隨想錄作者)曾經(jīng)寫過一篇著名的文章, Things You Should Never Do (1)?,他在文章中斷言,你永遠(yuǎn)不應(yīng)該從頭開始重寫一個代碼庫。他舉了 Netscape 公司的例子,他們花了好幾年的時間重寫軟件,最終公司在這個過程中死亡。一年前,我重讀了那篇文章,但還是選擇了從頭開始重寫我們的應(yīng)用,對,全部重寫。以下介紹為什么這么做,我們是如何成功的,以及一些關(guān)于你是否也應(yīng)該這么做的啟發(fā)式分析。
故事要從 2019 年 1 月說起。當(dāng)時,Remesh 還是一家比現(xiàn)在規(guī)模小得多的公司。當(dāng)時招聘了一些工程師,有 5 名工程師專注于產(chǎn)品開發(fā),還有一小部分工程師負(fù)責(zé)機器學(xué)習(xí)(ML)或 DevOps。盡管有這些工程師,但開發(fā)速度非常緩慢,簡單的功能需要很長時間才能完成,產(chǎn)品有很多已知的 bug 沒有修復(fù),而且整個產(chǎn)品看起來很長時間沒有明顯變化。
了解為什么會有這些問題是很重要。假設(shè)問題不在人上面,我們有優(yōu)秀的工程師(之后新版的成功也驗證了這一點)。問題主要出在代碼庫和流程上。我們所使用的歷史代碼庫,與團隊的技能和業(yè)務(wù)場景并不匹配,當(dāng)時的流程也鼓勵和依賴工程師垂直領(lǐng)域的知識,也沒有 "全棧"工程師。
2019 年 1 月代碼庫的狀態(tài)
舊版應(yīng)用的設(shè)計初衷與現(xiàn)在的版本截然不同。最初,Remesh 讓用戶在整個群之間或者一個人和一個群之間進(jìn)行雙向?qū)υ挕@?#xff0c;你可以讓 Democrats 和 Republicans 各自對話,互相了解對方,尋找共同點。或者,你可以讓一個城鎮(zhèn)的市長和他們的市民對話,以更好地了解他們需要什么、相信什么、想要什么。然而,當(dāng)我們找到了產(chǎn)品與市場的契合度后,用例也發(fā)生了變化。我們傾向于由一個單一的主持人與一群人交談。
需求變化的結(jié)果是,某些舊的設(shè)計方案不再有意義,schema 需要進(jìn)行重大改變。除了數(shù)據(jù)庫之外,代碼庫本身也很難理解,因為這些功能都是在沒來得及進(jìn)行大的重構(gòu)的情況下,被開發(fā)人員用螺栓連接起來的。在最需要重構(gòu)的地方,測試覆蓋率很差,因為這些代碼是最老的代碼,是在建立良好的測試實踐之前編寫的。
除此之外,語言和框架也不適合我們的團隊。后端代碼庫是用一種叫 Elixir 的語言開發(fā),而開發(fā)人員很少有人熟悉 Elixir。其中一個前臺代碼庫是用非常老舊版 Angular(我甚至不想去了解到底是哪個版本,往事不堪回首),我們還有兩個前臺是用 React 寫的。但工程師幾乎沒人了解其中一項技術(shù),更不用說這三個都會。使用的語言和框架并不適合團隊和我們的場景,這讓開發(fā)速度非常慢。
有哪些選擇?
毋庸置疑,我們的代碼庫需要一個重大的改變。當(dāng)你面前擺著一堆代碼,很難往前推進(jìn)時,大概有三個選擇:
-
重構(gòu)它,直到所有問題修復(fù)。
-
一口氣全部重寫
-
逐步小范圍重寫
對于前端,重構(gòu)并不是一個合適的選擇,Angular 版本已經(jīng)太老舊,以至于沒有任何明確的升級路徑可以升級到現(xiàn)代版本的 Angular(老實說,任何版本的 Angular 都興趣不大)。而且由于預(yù)計 UI 和 API 會有重大變化,所以重構(gòu)是不可行的。因此,在前端,我們只能選擇一次性重寫,或者逐步小范圍重寫。
后端有一些需要解決的問題 — 當(dāng)前的模式、語言和代碼庫都不適合我們的場景。我們使用了 Elixir,因為它有強大的并發(fā)支持,但我們最終不太需要這個功能,而且它反而陷住了我們:Erlang 虛擬機中處理并發(fā)的方式使得代碼分析變得非常困難,你知道計算的是什么,但不知道從哪里調(diào)過來的 — 祝你在性能調(diào)整方面好運。
Elixir 的代碼庫也限制了機器學(xué)習(xí)工程師對后端代碼庫的貢獻(xiàn):他們每天都在 Python 中工作,沒有時間深入學(xué)習(xí) Elixir。長話短說,我們想放棄 Elixir,轉(zhuǎn)而使用 Python 語言,因為這樣一來,整個團隊就可以參與貢獻(xiàn)后端代碼,這門語言可以解決我們的需求,而且分析代碼更加方便。
我們也有一些 "產(chǎn)品債務(wù)",老版本向用戶引入了一些新東西,他們接觸之后也逐漸喜歡上了這些理念,但最終效果并不理想。它們是局部的極端。如果我們要跳出這個局部極限,做出更好的東西。我們必須要做一次大的改版,在這個過程中,較小的迭代可能會不斷遇到用戶的阻力。去掉之前這些功能,需要同時做很多事情。
歸根結(jié)底,重寫的理由其實歸結(jié)為以下幾個因素:
-
希望團隊的每個成員都能為后端代碼庫做出貢獻(xiàn),而 Python 既容易學(xué)習(xí),又能在團隊中得到廣泛的認(rèn)可,所以很適合我們。
-
舊代碼庫非常脆弱,測試量少,重構(gòu)代碼庫是一個艱難的過程。
-
通過轉(zhuǎn)移到像 Django 這樣的強大的框架來提高效率,同時也能很多現(xiàn)成的東西節(jié)省時間(如 Django Admin)。
-
有機會根據(jù)從用戶那里了解到的東西制作一個全新的版本,然后可以輕松升級到新的版本,而不是在每一次小改動花時間與客戶解釋,持續(xù)一個 12 個月的拉力戰(zhàn)。這也使我們的客服團隊和銷售團隊的培訓(xùn)在最后成為一次性的批量培訓(xùn),而不是不斷地引入新概念。
為了達(dá)成這個決定,我們做了相當(dāng)廣泛的規(guī)劃。雖然整天談?wù)撁艚莺途媸裁吹?#xff0c;但這次實際是一個瀑布式的開發(fā) — 不是因為我們要實施瀑布式的計劃,而是我們發(fā)現(xiàn)重寫應(yīng)用程序需要不少時間,但重構(gòu)或零散地重寫需要更長的時間,而且不確定性要高得多。如果走重構(gòu)路線的話,我們要冒的風(fēng)險會更大。
最后,我們對自己的決定很有信心,而且公司的各個層面都支持我們。我們決定重寫,在讓產(chǎn)品向前發(fā)展的同時,修復(fù)過去幾年來的錯誤。
讓重寫開始吧。
進(jìn)展情況
我們在 2019 年 2 月開始重寫,在規(guī)劃出功能范圍之后,就開始啟動重寫,作為盡職工作的一部分,我們圍繞著我們要開發(fā)的功能,制定了一個非常堅實的計劃。這違背了敏捷的教條,但有了一個可以調(diào)整的計劃,有助于指導(dǎo)我們前進(jìn)的道路,看看是否偏離了軌道。當(dāng)我們與用戶(內(nèi)部用戶和一些外部客戶)在進(jìn)行測試的階段,我們最終確實偏離了不少計劃,更多的內(nèi)容會在后面說。
在經(jīng)歷了一開始的坎坷之后,構(gòu)建新版本的實際過程還算順利。對于工程師來說,切換到一個新的技術(shù)棧是痛苦的。雖然我們選擇了 Python 來達(dá)到最低的切入成本,但仍然有一些人需要學(xué)習(xí)。而且我們的后端工程師也沒接觸過 Django(但我們的首席前端工程師對 Django 有很深的了解)。同樣,在前端方面,很多人都知道 React,但很少有人對 TypeScript 有深入的經(jīng)驗,我們選擇 TypeScript 語言(這有一些故事要留待后文會說)。有了一些初步的學(xué)習(xí)時間后,我們都很快就有了相當(dāng)大的收獲。
這是我們第一個驗證得到的經(jīng)驗:即使在這個新的技術(shù)棧中經(jīng)驗較少,也能更快地構(gòu)建功能。要確定生產(chǎn)力的提高是來自于新的技術(shù)棧和新的代碼庫,而不是僅僅是一個空項目,這需要更長的時間,但我們最終還是達(dá)到了目標(biāo)。
首先做的一件事就是讓大家接觸數(shù)據(jù)庫。由于我們的目標(biāo)之一是減少信息孤島,讓工程師盡可能了解整個技術(shù)棧,所以我們引導(dǎo)一些對數(shù)據(jù)庫設(shè)計沒有什么經(jīng)驗的前臺開發(fā)人員,讓他們?nèi)ニ伎己驮O(shè)計最初的數(shù)據(jù)訪問版本,然后和整個團隊一起迭代。這使他們有能力去參與數(shù)據(jù)庫方面的問題。盡管他們已經(jīng)很久沒有參與這方面工作,但仍然表現(xiàn)出了這方面能力,并能提出一些真正具有挑戰(zhàn)性的問題。
在這之后,我們快速前行持續(xù)了幾個月,重寫了舊版本中熟悉的和感興趣的東西,并在不斷的優(yōu)化,使其更加好用。我們在合理的時間內(nèi)完成了一個非常好的項目。一開始,時間表非常樂觀,直到 6 月左右,我們一直在按計劃進(jìn)行。不過后來增加和改變了一些功能,因為我們知道沒有這些功能,新版本就不會成功。這讓項目速度慢了下來,但來自內(nèi)部研究人員、客服團隊和一些值得信賴的用戶的真實反饋,對我們項目成功是必要的。
在整個過程中,我們?nèi)〉昧艘恍┪乙詾榘恋某煽?#xff0c;不全是技術(shù)方面的。
-
團隊急劇增長。我們從最初的 4 個產(chǎn)品開發(fā)工程師開始,到現(xiàn)在的 9 個,這還不包括招聘了一個完整的 QA/SDET 團隊,增加了機器學(xué)習(xí)工程團隊的人員,以及招聘了 DevOps 工程師。而在這個急劇增長的過程中,并沒有因為增加人員而帶來通常的項目延遲 —— 相反,我們加快了速度(我認(rèn)為這主要得益于這是一個全新項目)。
-
改善了整個公司對工程團隊的看法。剛開始一段時間,我們在新功能的交付上有點慢,但至少可以快速地重寫已有的功能,并看到新功能也很快地被添加。有一次,我們做了一個很酷的演示,對 Django 的 Admin 進(jìn)行了實時編碼,以證明現(xiàn)在可以做的事情比以前快很多。雖然只是一個小小的演示,但很有效。
-
從一個有多個服務(wù)的面向服務(wù)的架構(gòu),變成了一個只依賴一個服務(wù)的單體架構(gòu),我們從一開始就開始設(shè)計容錯和橫向可擴展性。這在之前是一個很大的痛點。
-
極大地提高了迭代速度,很大程度上是因為我們有了一個新的架構(gòu),這個架構(gòu)適合我們的場景,而且是在一個大家(現(xiàn)在)都很樂意參與的技術(shù)棧中。錦上添花的是,機器學(xué)習(xí)團隊現(xiàn)在可以也確實偶爾給生產(chǎn)后端提交代碼。
主要經(jīng)驗
我們相信我們是成功的,當(dāng)然過程中也犯了一些相當(dāng)大的錯誤。
之所以成功,是因為我們一開始就對我們要打造的東西有一個清晰的愿景(一個真正的 MVP,我們知道舊產(chǎn)品是 "可行的",所以我們必須達(dá)到這個目標(biāo)或更少),我們根據(jù)需要削減范圍,以保持清晰的目標(biāo)。雖然我們沒有 "按時交付",但也沒有變成 Netscape 的方式。項目總工期不到預(yù)計的兩倍(基于完全復(fù)制舊產(chǎn)品功能的預(yù)期時間),但我們最終得到了一個更好的產(chǎn)品,并且有一些新的功能,比如上傳和發(fā)送視頻的能力,以及下載自動生成的 PowerPoint 報告等。
成功的另一個關(guān)鍵是盡早并經(jīng)常獲得反饋。在重寫過程中,我們經(jīng)常在內(nèi)部使用產(chǎn)品,發(fā)現(xiàn)關(guān)鍵的 bug 和性能問題。我們還定期舉行全公司的演示會,從幫助客戶成功、銷售、研究,以及從能夠容忍各種問題的早期試用用戶那里快速獲得反饋。
做錯的事情有哪些?我們曾經(jīng)引入兩個我們以前不怎么熟悉的技術(shù)。我們之前在一個原型中使用過 TypeScript,但我們對它沒有很深的專業(yè)知識。進(jìn)展雖然馬馬虎虎,但我們?nèi)匀徊幌嘈派a(chǎn)率會更高,缺陷率會更低;時間會證明,靜態(tài)類型的語言會更佳(如果有人對此有確切的研究,我很樂意你把它們發(fā)給我)。
另一個失誤是使用 GraphQL。我們在 REST 和 Redux 方面有相當(dāng)高的經(jīng)驗,但之前只在一個原型中使用過 GraphQL。現(xiàn)在回想起來,GraphQL 讓最初的原型開發(fā)速度快了很多,但長期的代價是,Apollo 中有些關(guān)鍵的設(shè)計決策我們并不認(rèn)同(比如沒有在前端暴露出檢測訂閱中斷開/重連的能力),而且在其后端的性能調(diào)優(yōu)經(jīng)歷也是一言難盡……那是我人生中非常艱難的一兩個月,我再也不想回去了。我們現(xiàn)在正在從 GraphQL 中遷移出來,對于性能關(guān)鍵的東西,會快速地進(jìn)行遷移,然后再慢慢遷移那些對請求性能容忍度較高的調(diào)用。
最后需要注意的是,在重寫的時候,你的團隊以及士氣會受到影響,你必須要積極應(yīng)對。一開始啟動一個新項目是相當(dāng)令人興奮的,但接下來的事情就是構(gòu)建已有的功能和修復(fù) bug,過了一段時間就會覺得很累。很欣慰看到我的團隊從構(gòu)建我們已有的功能到開發(fā)新的功能,我也意識到重寫工作真的很耗費精力。
我們成功地完成了重建,其中的一部分原因是平衡了新功能開發(fā)與舊代碼遷移。話雖如此,我希望我們在平衡方面能做得更好。下一次,我將集中精力確保我們有一個早期的 alpha 測試計劃,與幾個值得信賴的用戶一起進(jìn)行測試,以獲得定期的反饋和鼓勵,并讓大家對重建保持興奮。我還會確保我們在早期就加入大量的新功能,而不是發(fā)現(xiàn)大家都有點疲憊,才開始引入新功能。有些單調(diào)是不可避免的,但你可以減輕它。
你應(yīng)該這么做嗎?
根據(jù)我的經(jīng)驗,你也許不應(yīng)該像我這么做,如果你深信重寫永遠(yuǎn)不會是正確的決定那些文章。無論如何,你應(yīng)該默認(rèn)為 "不重寫" 的立場,然后非常努力地推進(jìn),并證明不重寫是正確的。
但有幾種情況,重寫可能是合理的。
-
如果你的架構(gòu)或模式與你的需求嚴(yán)重脫節(jié),而且沒有明確的遷移路徑,漸進(jìn)式更新架構(gòu)或模式變得非常困難。
-
如果這些問題嚴(yán)重拖累了你的團隊
-
如果你目前的技術(shù)棧限制了很多工程師的代碼貢獻(xiàn),并且技術(shù)棧培訓(xùn)也不太可行。
即使所有這些都符合你的情況,你也要進(jìn)一步考慮企業(yè)的實際情況,考慮到這對你的公司、你的團隊是否有意義。
有可能在更多的情況下,重寫是有道理的。辯解這一點很難,但它可能是值得走的一條路,而且可以成功地完成。
?
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
?
英文原文:
https://remesh.blog/refactor-vs-rewrite-7b260e80277a
總結(jié)
以上是生活随笔為你收集整理的重构,还是重写?(2020版)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 太难了~面试官让我结合案例讲讲自己对Sp
- 下一篇: 从一个工程师到管理员的经验分享