程式碼複用:DDD視角下的平衡藝術

papering發表於2024-10-30

程式碼複用: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 元的產品。
複用軟體的好處眾所周知,但我認為可以進一步拆分成兩種:
  1. 降低開發成本。透過整合業務中臺已有的支付,供應鏈等能力,可以快速支撐新的業務上線。
  2. 提升軟體產品的核心競爭力。已有的模組經過線上檢驗,其中積累了過去成功的經驗, 並且未來還會繼續積累,直接複用能夠大大提升產品的競爭力。

第一點是從成本角度,而第二點是從效益角度。
下文分別從這個兩個角度與成本進行比較, 引出兩位大師的觀點,就能更好地得出軟體複用的結論。

04



深淺模組:成本角度談複用

談到檔案系統,或者資料庫,應用肯定都是直接複用現有的開源軟體,或者公司內專業團隊定製的。不可能複製一份資料庫程式碼到應用中。一方面是沒這實力,更重要是不划算。
檔案系統對上層提供了非常簡單的檔案模型,資料庫對應用也提供了非常好理解的表模型。而他們的實現非常複雜,需要考慮併發,資料完整性,事務等一系列問題。相比理解他們的實現,學習模型和介面成本幾乎可以忽略不計。

圖片

學習 SQL 相比學習 資料庫實現 的成本,從相關書籍的厚度上就能看出一二,更何況它們的閱讀難度相差也很大。

上面的案例有共同的特點,即模組的介面很簡單,但是提供的功能卻是深刻的。這個時候複用就非常的合算。
這剛好就是 John Ousterhout 教授(Raft 的發明者)在其著作 《軟體設計哲學》中提到 深模組 的概念。
深模組在簡單的介面後隱藏了許多功能。深模組代表很好的抽象,其內部複雜性只有很小一部分對其使用者可見。
其反例就是淺模組, 淺模組介面很複雜,提供的功能卻不多。在專案中經常會下面這樣的程式碼:
public void addParameter(List<String> params, String param) {    params.add(param);}

它接收兩個引數,卻只實現了一個最簡單的列表增加元素功能,尋找和複用它的成本已經超過了複用的好處。
淺模組的介面複雜度和實現複雜度接近,與其去了解模組的介面,開發人員還不如自己重新實現一遍。

圖片

圖片

《軟體設計哲學》書中的配圖,方塊的寬度代表模組介面的複雜程度,深度代表功能的深刻程度,介面應該越簡單越好,功能應該越深刻越好。深模組就是介面簡單但是功能深刻的模組。


05



塑造產品的核心競爭力:效益角度談複用

什麼情況下,複用能夠提升產品的核心競爭力呢?
Supercell 遊戲公司將之前的爆款中備受玩家歡迎的風格,素材和程式邏輯沉澱下來,透過複用之前積累,可以快速產出新的爆款。
釘釘的審批流程配置功能經過多年的迭代,操作習慣已經深入人心。後來釘釘又推出 CRM 應用,直接複用這套配置介面和邏輯, 雖然需要開發一些適配邏輯,但大大降低了使用者的學習成本, 提升了競爭力。
上面的兩個例子剛好就代表了兩種提升產品核心競爭力的邏輯:
  1. 複用之前具有競爭力的技術模組,讓過去的成功經驗助力未來的產品成功
  2. 給使用者提供一致的體驗,考慮使用者的使用習慣,降低學習成本

複用不同模組能取得效果的程度也是不同的,複用什麼樣模組更有可能獲得上述兩點效果呢?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/

相關文章