精讀《維護好一個複雜專案》

黃子毅發表於2022-11-22

現在許多國內網際網路公司的專案都持續了五年左右,美國老牌公司如 IBM 的專案甚至持續維護了十五年,然而這些專案卻有著截然不同的維護成本,有的公司專案運作幾年後維護成本依然與初創期不大,可以保持較為高效的迭代速度,但有的專案甚至改幾個文案都會導致線上事故,研發效率變得越來越慢。

根據筆者的經驗,嘗試總結一些持續維護專案變得難以維護的原因,以及如何設計才能保持良好的可維護性。

精讀

心態

如果不真心對待自己的專案,其實是很難做到良好可維護性的,所以第一點就是需要一個良好的心態。

作為專案管理者,一個專案一旦交給一位同學開發,那麼就要完全信任這位同學的能力,因為實際上你已經不可能實質性的影響到開發細節了。有人可能覺得好的流程或者事後 CodeReview 能發現一些問題,但這永遠是杯水車薪,比如下面這個例子:

小張接到任務研發透視表,要求這個透視表具有良好的開發體驗並做好單測。

那怎麼樣做單測才算是有效的,如何同時保證開發體驗呢?不同人會有不同的想法,也會有不同的結果。

有主人翁心態的小張

對於一個有一定經驗,又對專案真正上心的小張來說,開發過程可能是這樣的。

首先上來先寫主要功能,比如考慮資料模型、繪圖技術方案後,決定採用圖形語法方式定義資料結構,在做了一系列高效能前置考慮後,快速做出來了一個原型,包含表格的渲染、操作、翻頁、凍結等等功能。

但隨著需求的深入,小張發現做到下鑽、排序時,不知道為何影響到了列凍結的功能,而程式碼架構其實沒什麼大問題,抽象的也很好,主要就是一些細節的程式碼呼叫漏掉了,只要補上就立馬打通了任督二脈,整套功能再度行雲流水了起來。但不知道下次做樹狀展示結構時會不會又把之前的功能影響了,這始終是個隱患,於是小張開始思考先把單測加上再繼續開發功能。

由於出問題的場景有很小部分是大量操作後偶然引發的,普通的函式式單測也無法保證覆蓋的全面,因此小張決定做一個單測錄製功能,他首先把對錶格的所有操作 Action 化,讓一套 json 可以描述所有使用者操作,然後又在本地開發介面做了一個單測錄製功能,即在頁面上對錶格功能拖拖拽拽時,就會實時生成這套使用者操作 json,再把當時頁面結構與內部狀態記錄下來作為對比依據,單測就還原這套 json 並與基準狀態做對比就行了。

小張很快錄製了很多原子操作的單測,比如表格的各種空資料狀態、單行單列渲染、列凍結行凍結;然後又把一些功能混合的場景結合起來,比如列凍結時排序,翻頁後進行下鑽;最後又把一些隨機複雜的功能組合在一起,形成一些日常容易出問題的特殊單測 case,比如表格單頁後突然清空資料,再強制凍結第二列,再灌入3列資料並對第2行做排序,再取消列凍結並翻到第4頁。以後每當遇到一個邊界 case 時,小張都會把這個問題 case 記錄到單測,驗證確實執行失敗,再進行修復,直到包含這個單測在內的所有單測都驗證透過後,才算開發完成。

打工人小張

對於為了混口飯吃的小張來說,開發過程可能是這樣的。

首先上來寫主要功能,把各種表格功能做完後,也遇到了一樣的邊界 case 難題,此時小張本來想 case by case 修復,但又想到 leader 要求他寫單測,覺得倒也不壞,就建立了單測目錄。

怎麼寫單測呢?首先小張把遇到的問題修了,畢竟誰也不希望自己手裡的 bug 太多,但至於錄到單測就太麻煩了,反正大家也不知道這個 case,修掉了就再也不會出來了吧,那就只把 leader 要求的幾個基本功能單測加上去,看下覆蓋率也達到硬性指標就行了。

大團隊程式碼總是容易走向混亂

假設你是 leader,你不知道自己團隊的小張到底是主人翁小張還是打工人小張,企圖透過 code review 來統一提升團隊的程式碼質量,實際上可行嗎?

如果不幸遇上了打工人小張,他在 code review 時展示的程式碼結構就不是能做整體單測的抽象,你只能看著單測檔案硬提一些比如 “多加一些單測,多考慮一些情況” 的建議,實際上完全達不到主人翁小張做的效果。

這背後的原因是影響程式碼質量的因素太多,比如 Action 化,比如各種極端 case 的錄入,比如全流程的單測形式,這些對程式碼來說都是質變,但 code review 時看到的程式碼就是不夠抽象,不夠 Action 化的,不可能把程式碼推翻重寫一遍,只能在已有程式碼基礎上提最佳化建議,而到這個時候,神仙也沒法讓打工人小張的程式碼最佳化為主人翁小張的,除非推翻重寫。

這就是心態的影響力,能把專案做好的細節很多,而且細節之間還是環環相扣的,比如不把程式碼 Action 化就不方便做整體單測,但如果開發者打一開始就沒想好好設計,code review 時又有多少人能想到這一點呢?想到了此時再提可能也為時太晚,一切都已成定局。

這些年筆者看過不少久經歷史的程式碼,因為大公司有大量的開發者維護同一個專案,每個人開發時的心態都各有不同,會發現總能看到那些模組是用打工人心態做出來的,而你想徹底最佳化就只能徹底重寫,但礙於專案體量太大時間上不允許,只能沿著打工人思路洋洋灑灑的繼續寫下去。

所以擁有一個良好,正面或者說積極的主人翁心態來寫程式碼,一般來說都可以維護好複雜專案。

解耦

複雜專案的複雜指的是什麼呢?是指功能多嗎?其實不然。

如果僅從功能多就判定這個專案複雜,那我們身處的社會才是最複雜的系統,但社會中的每個玩家都沒有覺得吃穿住行很難,核心原因就在於瞭解我們用到的場景只需要少量的知識,而做出一個行動要得到正確的結果,也不會造成太大的影響。比如出門買菜,只要做個公交車到菜市場,掃一下碼就完成了交易,而不需要對背後的城市公交體系與菜市場背後的金融體系有任何深入的瞭解,你不需要理解公交車是哪兒來的,菜農手裡的菜是從哪兒收購的。

但程式碼世界就很有趣了,在程式碼世界買個菜可能會導致世界毀滅。這就導致每一個專案開發人員,哪怕是去買個菜,也要受過總統級訓練,對各種國家級大事做出正確的預案,為什麼會這樣呢?

因為程式碼世界的邏輯是不同開發者碼出來的,在實現世界底層邏輯時可能就埋下了耦合的種子,導致你不知道為什麼買菜會觸發那麼嚴重的事情。舉個例子,改一個文案導致系統崩潰,原因可能是某處錯誤兜底邏輯用字面量判斷了這個文案,而你把文案改了,這個判斷就失效了。有的程式設計師挺難的,在這種專案環境下生存,每一步修改都要小心翼翼。

這個問題的解決辦法就是解耦,在這裡我們不細說具體怎麼解耦,因為每個場景的解耦方式都不同。我們只需要理解幾乎所有的業務邏輯都可以用解耦的方式做,就行了。只要你按照這樣的大思路去設計系統,不論路徑是怎樣的,最終都能設計出一個漂亮的系統級方案。

比如做一個 BI 系統,看上去裡面有各種複雜的模組可能會產生相互影響,比如資料處理、儀表盤搭建、大屏搭建、圖表、GIS 地圖等,在設計之初就要假裝其他模組不存在,來考慮每個模組必要的輸入是哪些。

比如佈局,它僅僅用於對畫布進行佈局,為了保證佈局系統是完全解耦的,必須讓專案支援在無佈局的環境下執行。為了做到這一點,就必須讓佈局真的 “只做佈局”,而不儲存當前畫布結構,這樣才不會因為佈局系統被移除時,影響元件的聯動,因為元件聯動需要利用畫布結構 API。

圖層列表也可以和佈局解耦,因為圖層列表只關心畫布的元件樹結構,而不關心佈局是如何實現的,所以畫布的元件樹結構就像生活中的金錢,大家都可以用它交易,而無需關心它流向了何方,被誰使用。

資料邏輯與畫布結構無關,只需要關心表示式以及使用者對維度度量的配置、聚合方式以及圖表本身的特性進行查詢 sql 拼接即可,唯一用到的通用資源是當前元件例項資訊修改後,需要更新到畫布的元件樹上。

社會也是建立在這種底層認同上,才能這麼解耦的,所以在複雜專案中一定要有一個大家都認可的底層概念,這個概念應該儘可能通用化(想想金錢什麼都能買,如果只能買蔬菜就麻煩了)、貫穿整個業務邏輯(金錢是現代社會任何交易都必須的媒介)。

許多專案被詬病難改,往往是沒有遵循這條邏輯,硬生生把可以不相關的概念耦合了。比如某個篩選器條件變化時,對某個元件做特殊操作,這個場景可以控制反轉為,這個元件在接收到某些篩選條件時,自己做特定的操作。因為對 BI 系統來說,篩選器的輸出要作為圖表繪圖的輸入,在這個底層框架下,就不要再開闢一條篩選器關心到具體圖表的邏輯了。

總結

維護好一個複雜專案很難,這次分享了兩個實踐中有用的方案,第一個抱有主人翁心態設計程式碼,要在設計之初就做好考量,不要寄希望於對沒有好好設計的系統做縫縫補補。第二是深入理解為什麼現代社會的運作巧妙之處,儘可能把程式碼架構組織一定程度對映到社會的運作機制上,目前來看,社會最適合程式碼借鑑的思路就是解耦,再利用龐大的分工協作網路完成單人無法完成的工作。

討論地址是:精讀《維護好一個複雜專案》· Issue #454 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章