程式碼複用:DDD視角下的平衡藝術 https://mp.weixin.qq.com/s/5gIBJByRZfNPbh6yjAvj9w
程式碼複用:DDD視角下的平衡藝術
這是2024年的第76篇文章
( 本文閱讀時間:15分鐘 )
01
引言
剛工作時,程式碼寫得不太好,師兄每次 CR 程式碼,總是會指著螢幕裡的一坨程式碼說 “把它抽成一個類或函式”;“為什麼呢?寫在一起不是挺好的嗎?” 我反問道;師兄老道地回答 “為了方便複用”;我彷彿若有所得,回到工位上把那些很長的程式碼全部抽象成了類和函式,感覺今天又有所成長。
但是隨著工作經驗的增加,我對此又產生了困惑。隨著業務發展得越來越複雜,我當初寫的那個類被大量複用,為了適應不同的場景,裡面充滿了 if...else...;最能代表複用的業務中臺,因為分支太多,釋出和開發無比複雜,很小的一個改動卻需要拉一堆團隊討論。
所以類和函式的存在究竟是為了什麼?只有站在更高的視角才能解決我的困惑,這也是本文的內容。
根據奧卡姆剃刀原則,本文其實用一句話就能概括, 它也是 《複雜軟體設計之道》 中我最喜歡的一句話 :類和函式不是為了複用而存在,而是他們本來就 “應該” 在那裡。
如果您對這句話已經意會了,可以直接跳到評論區聊一聊看法了。
下文中主要結合歷史上各位軟體泰斗的觀點,分別從成本和效益角度聊聊 “應該” 一詞的含義。
02
DRY vs 重複程式碼:誰更好嗎?設計模式的 DRY 原則(Don't Repeat Yourself)讓我們儘可能地不要編寫重複的程式碼。
但是在複雜工程中導致的問題就是,DRY 的函式會被大量的地方引用,導致其內部邏輯需要考慮各種情況,邏輯及其複雜,修改風險也極高。
這麼看來,DRY 也沒有那麼好,重複程式碼反而可以降低後續的修改風險,日後可以根據業務需要再進行靈活整合。
在 《架構整潔之道》中提到,“拖延決策” 也是優秀架構設計的特點之一。因為隨著軟體的開發和業務的迭代,我們掌握的資訊越來越多,後期做出的決策肯定比專案早期的草率決定要靠譜。《複雜軟體設計之道》中吐槽道:架構師們總是在只掌握 20% 資訊的情況下,就已經做出了 80% 的決策。
大師們的原則常常是相互矛盾的,沒有什麼絕對的更好或者更壞。下表簡要總結了下 DRY 與重複程式碼各自的優缺點:
從上表可以看出,重複程式碼和 DRY 很難說孰優孰劣,有時候費了半天勁抽取程式碼,反而系統複雜性更高了。符合設計原則的程式碼不一定是好程式碼,不符合設計原則的程式碼不一定是壞程式碼。
透過純粹設計原則的角度是看不出來軟體設計決策是否正確的,必須從更高的視角出發才行。
03
複用是一個權衡
我們常常被教育複用的好處,而忽視了複用的成本。為了複用一個程式碼模組:- 首先我需要知道可複用構件的存在
- 然後瞭解其中的結構和介面
- 對接模組的介面,並且測試無誤
- 最後,只是會用還不夠,如果線上出現,我必須保證自己對它有足夠的瞭解,可以去排查該模組的問題
而只要有成本的東西就是需要權衡的。沒人願意花費 10 元價格,只買回來一個價值 8 元的產品。
複用軟體的好處眾所周知,但我認為可以進一步拆分成兩種:
- 降低開發成本。透過整合業務中臺已有的支付,供應鏈等能力,可以快速支撐新的業務上線。
- 提升軟體產品的核心競爭力。已有的模組經過線上檢驗,其中積累了過去成功的經驗, 並且未來還會繼續積累,直接複用能夠大大提升產品的競爭力。
第一點是從成本角度,而第二點是從效益角度。
下文分別從這個兩個角度與成本進行比較, 引出兩位大師的觀點,就能更好地得出軟體複用的結論。
04
深淺模組:成本角度談複用
談到檔案系統,或者資料庫,應用肯定都是直接複用現有的開源軟體,或者公司內專業團隊定製的。不可能複製一份資料庫程式碼到應用中。一方面是沒這實力,更重要是不划算。檔案系統對上層提供了非常簡單的檔案模型,資料庫對應用也提供了非常好理解的表模型。而他們的實現非常複雜,需要考慮併發,資料完整性,事務等一系列問題。相比理解他們的實現,學習模型和介面成本幾乎可以忽略不計。
學習 SQL 相比學習 資料庫實現 的成本,從相關書籍的厚度上就能看出一二,更何況它們的閱讀難度相差也很大。
上面的案例有共同的特點,即模組的介面很簡單,但是提供的功能卻是深刻的。這個時候複用就非常的合算。
這剛好就是 John Ousterhout 教授(Raft 的發明者)在其著作 《軟體設計哲學》中提到 深模組 的概念。
深模組在簡單的介面後隱藏了許多功能。深模組代表很好的抽象,其內部複雜性只有很小一部分對其使用者可見。
其反例就是淺模組, 淺模組介面很複雜,提供的功能卻不多。在專案中經常會下面這樣的程式碼:
public void addParameter(List<String> params, String param) {
params.add(param);
}
它接收兩個引數,卻只實現了一個最簡單的列表增加元素功能,尋找和複用它的成本已經超過了複用的好處。
淺模組的介面複雜度和實現複雜度接近,與其去了解模組的介面,開發人員還不如自己重新實現一遍。
《軟體設計哲學》書中的配圖,方塊的寬度代表模組介面的複雜程度,深度代表功能的深刻程度,介面應該越簡單越好,功能應該越深刻越好。深模組就是介面簡單但是功能深刻的模組。
05
塑造產品的核心競爭力:效益角度談複用
什麼情況下,複用能夠提升產品的核心競爭力呢?Supercell 遊戲公司將之前的爆款中備受玩家歡迎的風格,素材和程式邏輯沉澱下來,透過複用之前積累,可以快速產出新的爆款。
釘釘的審批流程配置功能經過多年的迭代,操作習慣已經深入人心。後來釘釘又推出 CRM 應用,直接複用這套配置介面和邏輯, 雖然需要開發一些適配邏輯,但大大降低了使用者的學習成本, 提升了競爭力。
上面的兩個例子剛好就代表了兩種提升產品核心競爭力的邏輯:
- 複用之前具有競爭力的技術模組,讓過去的成功經驗助力未來的產品成功
- 給使用者提供一致的體驗,考慮使用者的使用習慣,降低學習成本
複用不同模組能取得效果的程度也是不同的,複用什麼樣模組更有可能獲得上述兩點效果呢?DDD 中對子域的劃分或許能夠給我們答案,在 DDD 中軟體存在三種子域:
- 核心子域
- 特點:能夠給公司帶來核心競爭力的領域模組,擁有很高的複雜度和差異化價值
- 案例:比如滴滴的司機排程演算法,支付寶的交易系統,釘釘的 IM 系統等等
- 複用策略:屬於該子域的模組應該儘可能地複用, 將其競爭力也注入到其他產品,甚至投入精兵強將,提升其可擴充套件性,進一步拉開和競爭對手差距
- 支援子域
- 特點:用來支撐核心子域,但是不能帶來競爭力
- 案例:比如運營管理系統,後臺排查系統等等
- 複用策略:因為不能帶來核心競爭力,不如各個業務根據自己需求,使用腳手架快速搭建,定製起來還更加方便
- 通用子域
- 特點:通用的業務或者技術問題領域, 比較複雜, 卻不能給企業帶來核心競爭力。好在一般有現成的解決方案,可以直接採購
- 案例:比如財務系統,可以直接採購用友,金蝶;分庫分表,訊息佇列可以直接使用開源軟體,或者購買雲上解決方案
- 複用策略:儘可能複用,但是複用的目的與核心子域不同,主要是為了降低研發成本
以 DDD 中經典的貨運管理系統為例(簡化):
相比對於業務的助力,複用的成本就顯得微不足道了。
因此 DDD 要求技術和業務深度結合,如果不瞭解業務的話,單從設計原則角度,很難理解為什麼要複用一個技術模組。
成功的設計來自對業務問題的深刻理解。最符合其業務子域的地方,才是類/函式 應該 在的地方。
06
總結
世上只有一種英雄主義,就是在認清生活真相之後依然熱愛生活工程師對技術也只有一種熱愛,就是當發現任何技術都無法代替對業務的深入認知後,依舊熱愛程式碼。
DDD 的思想和工具能夠幫我們站在更高的視角,從業務分析的視角看待複用的成本和效益,幫助我們更好地做出決策。
參考書目
[01] 《複雜軟體設計之道》https://book.douban.com/subject/35216922/[02] 《架構整潔之道》
https://book.douban.com/subject/30333919/
[03] 《軟體設計哲學》
https://yingang.github.io/aposd-zh/