聊一聊:一個大型軟件系統(tǒng)該如何重構
1、 為什么需要重構
在互聯(lián)網(wǎng)行業(yè),每當新員工入職一家新公司時,都要學習一套新的軟件系統(tǒng)。如果該公司的代碼非常規(guī)范,架構設計非常合理,那么新員工上手的速度會非常快。當然,你這個螺絲釘?shù)慕巧簿头浅C黠@了。另一方面,如果面對『屎山』一樣的祖?zhèn)鞔a,就會有很多抱怨,學習起來也很痛苦。
從質量上,我把軟件大致分為以下幾種類型:
- 第一種:它們對穩(wěn)定性、規(guī)范性要求非常高,所以代碼中異常判斷、校驗非常多,代碼看上去就很冗余,最典型代表是華為的電信級產(chǎn)品。
- 第二種:寫得就很隨意,風格各式各樣,代表的例子是開源項目。
- 第三種:也是最糟糕的,員工在 PM(產(chǎn)品經(jīng)理)的高壓下日夜加班趕出來的,毫無設計風格,能跑就行,追求最快速度地實現(xiàn)。
重構應該也是互聯(lián)網(wǎng)公司開發(fā)工作的一部分吧。當一個軟件系統(tǒng)需要重構時,必然是因為以下某種原因:比如二次開發(fā)難度越來越大、新員工上手越來越困難,每個模塊只有特定的人懂;面對日益增長的功能需求,現(xiàn)有架構已經(jīng)滿足不了;模塊耦合非常嚴重,導致不同的開發(fā)團隊之間互相依賴,嚴重阻礙了開發(fā)進度。
1.1 二次開發(fā)的難度越來越大、新員工上手成本越來越高
模塊之間耦合嚴重,類間調用關系形成雙向依賴。大量的 if/else 分支、運行流程分支太多,圈復雜度爆表,根本理不清。當我們想要增加一個新功能,即使這個功能很小很小,牽扯的鏈條也非常長,涉及需要改動的周邊函數(shù)、文件數(shù)量巨多。
1.2 腐朽的軟件架構無法滿足日益增長的功能需求
軟件系統(tǒng)從最初的 demo 開始,不斷完善,一點點添加新功能,以適應不同的應用場景。比如滴滴這款軟件,最初只要把司機端和乘客接進來就行,加上調度系統(tǒng)負責派單,用戶規(guī)模也比較小;后來,用戶規(guī)模陡增,需要擴展服務器數(shù)量,又牽扯到負載均衡、消息中間件、高并發(fā)等需求 ;再后來,添加各種服務類型,比如順風車、專車、豪華車等等,然后又是各種紅包、優(yōu)惠券等。
1.3 模塊耦合嚴重,無法上云
微服務的前提條件就是模塊間能解耦,這不僅是上云的需求,也能提高研發(fā)團隊整體的開發(fā)效率,更重要的是為了實現(xiàn)服務編排,可以給任意子的服務提供靈活的資源,從而最大化集群的資源利用率,也就是說能更好地做到彈性擴縮容和容錯。
1.4 新功能一起考慮進去
傳統(tǒng)概念中對代碼重構的理解是『不引入任何新功能』。我的看法是,代碼重構和新功能開發(fā)結合起來,這樣更有利于最大化重構效果。
2、 重構設計的指導原則
重構也是軟件架構設計的一種,這里我稱之為『重構設計』。
首先,你要清楚重構的目標是什么。比如側重滿足二次開發(fā),或者側重模塊解耦,或者兼容各種硬件平臺、編程語言等等。
其次,你要對基本的軟件架構和軟件設計風格有清晰的了解,以下是一些必備技能:
2.1、unix 編程藝術
- 模塊原則:使用簡潔的接口拼合簡單的部件
- 清晰原則:清晰勝于技巧
- 組合原則:設計時考慮拼接組合
- 分離原則:策略同機制分離,接口同引擎分離
- 簡潔原則:設計要簡潔,復雜度能低則低
- 吝嗇原則:除非卻無他法,不要編寫龐大的程序
- 透明性原則:設計要可見,以便審查和調試
- 健壯原則:健壯源于透明與簡潔
- 表示原則:把知識疊入數(shù)據(jù)以求邏輯質樸而健壯
- 通俗原則:接口設計避免標新立異
- 緘默原則:如果一個程序沒什么好說的,就沉默
- 補救原則:出現(xiàn)異常時,馬上退出并給出足夠多的錯誤信息
- 經(jīng)濟原則:寧花機器一分,不花程序員一秒
- 生成原則:避免手工hack,盡量編寫程序去生成程序
- 優(yōu)化原則:雕琢前要先有圓形,跑之前先學會走
- 多樣性原則:絕不相信所謂”不二法門“的斷言
- 擴展原則:設計著眼未來,未來總比預想來得快
2.2、 設計模式 + SOLID
23 種設計模式,你不一定要完全了解代碼怎么寫,但一定要知道每一種設計模式背后的設計思想是什么。有一段時間,我試圖在我的代碼應用各種設計模式,可最終代碼看起來特別冗余而且不是那么必要。從個人經(jīng)驗上來講,平時業(yè)務代碼中用設計模式的場合非常少,最常用的無非是工廠、適配器、責任鏈等,而且效果并沒那么大,設計模式真正適合的場合是更高層級的,比如模塊間設計等等。
單一職責、開閉原則、里氏替換、迪米特法則、接口隔離、依賴倒置。
2.3、領域驅動設計(DDD)
有了解過一些,沒有親身實踐過。它大體上就是模塊解耦和分層的思想:API(對外訪問層)、Domain(領域層)、Repository(數(shù)據(jù)源訪問代理層)及基礎設施層(DB、Redis、HTTP、RPC 等)
2.4、逐行代碼的重構方法
參考馬丁.福勒那本經(jīng)典的《重構:改善代碼的設計》,比如過長的函數(shù)、過長的參數(shù)、數(shù)據(jù)泥團怎么處理等等。
這里,也說下個人的小建議:比如縱向的調用關系變?yōu)闄M向的,減少函數(shù)調用棧深度;不要過度封裝。相信用戶能找到底層類的實現(xiàn)接口;邏輯上相關的代碼物理上盡可能放在一塊;對于某個小的具體功能,涉及的鏈條越短越好;面向接口編程;訪問 A 表數(shù)據(jù)的 class 中不能存在訪問 B 表數(shù)據(jù)的 function;模塊對外暴露的接口部分,數(shù)據(jù)類型的選擇上盡量做到寬進嚴出(接口要考慮通用性);寫操作接口,接收參數(shù)盡可能少;讀操作接口,返回參數(shù)盡可能多;減少不必要的類和數(shù)據(jù)結構等等。
2.5、微服務
2.6、云原生
毫無疑問,云是未來數(shù)字世界的基礎設施。
2.7、插件思想
比如約定/注入插件、事件插件、插槽插件等等。
好了,這里不再列舉了,因為根據(jù)哈弗大學心理學博士米勒的研究,對于每一類產(chǎn)品,用戶最多只能記住七類品牌,這里我也就列七個重構原則~
3、 如何撰寫重構設計文檔
以面向對象語言為例,這里我把它分為了幾個步驟:
- 舊系統(tǒng)的類間調用關系圖 vs 新系統(tǒng)的類間調用關系圖
- 舊系統(tǒng)的整體架構圖 vs 新系統(tǒng)的整體架構圖
- 舊系統(tǒng)的運行流程圖 vs 新系統(tǒng)的運行流程圖
- 分模塊逐一拆解:先畫出重構前的樣子,再畫出重構后的樣子
- 新增功能有哪些,怎么在重構后的系統(tǒng)里添加
- 考慮對舊系統(tǒng)的影響:兼容性問題
- 考慮實施可行性: 重構成本和收益之間如何權衡
4、重構的具體執(zhí)行步驟
總體上有兩種方式:
- 新建分支(不推薦)
- 新建目錄(推薦),這樣能保證原來舊的那套代碼能用。這里又有兩種方式:
- 做減法(先跑通舊的代碼,然后一點點刪除冗余代碼,或重構具體子模塊)--推薦,因為如果實在沒有開發(fā)時間了,某些未完成的模塊還可以沿用以前的老代碼。
- 做加法(從每個子功能開始重構,相當于重新構建這套軟件系統(tǒng))-- 不推薦,因為時間成本不可控
5、總結
用設計原則和架構理論武裝自己,閱讀優(yōu)秀的源碼驗證理論,深入理解具體業(yè)務,方能設計出一套優(yōu)雅的軟件系統(tǒng)。