在使用 DDD 的思想時,最讓人迷惑的就是如何組織程式碼,也就是通常所說的系統架構的問題。在前面提到 DDD 可以很好地指導程式碼組織,其中舉了兩個例子,單體和微服務架構下 DDD 如何指導程式碼的組織方式。令人沮喪的是,大部分應用系統既不是完全的單體系統,也不是純粹的微服務架構,而是出於某種中間狀態。
無論我們使用單體、SOA、微服務、中臺或者其他架構,都需要解決如何組織程式碼這個問題,DDD 並不是一個技術,而是指導我們組織程式碼的一種思想,這種思想也並不是憑空出現的。
就程式碼組織這個問題,看起來沒有什麼技術含量,但實際上非常重要,軟體工程發展過程中出現過三次危機,軟體危機泛指在計算機軟體的開發和維護過程中所遇到的一系列嚴重問題,程式碼的組織和大規模協作是其重要的組成部分。
結構化程式設計解決了第一次軟體危機。60年代~70年代計算機剛剛投入商業使用,主要的程式設計方式還是組合語言在特定的機器上編寫程式。當軟體規模較小,基本上處於計算機科學家個人編碼設計、使用的方式。隨著軟體規模擴大,複雜度增加,依賴特定機器、無結構化的程式設計方式無法應對軟體的發展,帶來了第一次軟體危機。為了克服這個問題,業界提出了”軟體工程“的概念,1972 年 C 語言的出現,解決了程式碼結構化、抽象性、可移植的問題。面向物件解決了第二次軟體危機。隨著軟體在商業中大規模使用,軟體變得原來越複雜,即使結構化的 C 語言也無法滿足業界對可維護性、可拓展性的需求。標誌性的事件是 IBM 公司開發的 OS/360 系統失敗,該系統有 4000 多個模組,約 100 萬條指令,以及大量的 bug。面向物件的程式語言,Java、C#、C++ 出現,面向物件帶來了更自然地程式碼組織方式,軟體開發變得越像建築業。第三次軟體危機。第三次軟體危機還沒有一個明確定義,通常來說就是網際網路行業興起,軟體變得越來越複雜,需求越來越多變。軟體開發從建築業變成了服務業,需要隨時響應變化,在軟體行業表現為瀑布開發越來越不可行,敏捷開發越來越重要。從技術上表現為單機開發越來越不可行,分散式系統是必然的趨勢。每一次危機的解決,都是建立在前一次的基礎之上的。面向物件是建立在結構化程式設計之上的,敏捷也是建立在瀑布之上的,而不是推翻前者。DDD 還停留在面向物件這個階段,可以用來指導分散式系統設計,應對越來越複雜的應用系統,DDD 也不是面向物件思想的替代者。
DDD 的程式碼組織形式眾說紛紜,並沒有一個標準的程式碼架構。為什麼會這樣呢?實踐中我們發現,不同公司、專案的業務背景不一致,架構不一致,架構的演化層次不一樣(檢視另外一篇文章《架構的演進》),標準的程式碼架構並不適合每一個公司。
當我們的系統架構從單體往 SOA、微服務、中臺演變,無論名稱如何變化,實際上都是分散式系統,只不過分散式的程度不一致而已。所以我們需要將問題拓展到分散式系統這個更大的概念上,再來談 DDD 的程式碼組織形式才有意義。
我們看一下分散式系統下一個定義:
分散式系統是一組電腦,透過網路相互連線傳遞訊息與通訊後並協調它們的行為而形成的系統。——維基百科
從廣義的分散式系統定義上來看,現在的網際網路應用基本上沒有不是分散式的了。分散式系統不是軟體工程師主動選擇的結構,而是業務逼得這樣選擇。阿里巴巴帶動的去 IOE (去掉IBM的小型機、Oracle資料庫、EMC儲存裝置,代之以自己在開源軟體基礎上開發的系統)就是一個很好的體現。
在這樣的一個思維方式下,單體系統是隻有一個計算節點的分散式系統,那麼 DDD 在單體應用下的經驗也可以應用起來。我沒有找到一個專業術語描述分散式系統程度,這裡請允許我創造一個新詞,分散式級別。
分散式級別為了解決業務上的問題,使用者量大、業務規模大,當用戶量增長到無法被容忍時,我們引入分庫分表(分散式資料庫)、垂直拆分業務(微服務)。
我們會將系統變得越來越複雜,然後不得不解決各種分散式系統下的新問題,業務上面臨的問題被轉移到技術上,從而業務才有可能持續性的發展。我們面臨的問題不會消失,只會從一個地方轉移到另外一個地方,轉移到我們能容忍的地方,比如轉移到雲上,然後透過購買服務解決。
系統中節點角色越少,需要解決的分散式問題則越少,可以認為這是低級別的分散式系統。低級別的分散式系統 架構基本上沒有什麼分散式問題存在,目前主流的小專案透過 Nginx 讓應用水平拓展 + 主從資料庫的架構可以看做低級別的分散式系統。
系統中節點的角色越多,應用垂直拆分,需要解決的分散式問題就越多,遇到的技術挑戰也越多,我們可以認為這是高級別的的系統。應用系統的例子就是微服務架構,另外一個例子就是大資料平臺。
我把分散式級別做了如下劃分,基本上可以囊括目前網際網路應用系統的主流架構:
準單體系統低級別分散式系統高級別分散式系統複雜分散式系統在微服務專案中經歷過痛苦的開發者應該所有體會,全世界開發者貢獻了大量的開源軟體嘗試解決這些問題,後面詳細介紹每一個問題如何具體解決。
清醒的使用 DDD上面這些分散式系統的問題,DDD 都解決不了。DDD 的作用只有一個:在單體中劃分模組,在分散式系統中劃分服務。 服務劃分的良好,關聯查詢、授權、分散式一致性等問題可以被很好的解決,也就是我們常常說的解耦。
但是就這一個作用,對於做應用開發的業務系統來說至關重要,雖然對於專門解決技術複雜度問題的雲廠商來說用處不大,所以最好讓 DDD 在合適的地方發揮作用。高級別的微服務系統的修改成本如此之高,以至於服務劃分錯誤幾乎沒有能力調整回來,甚至導致很多網際網路公司就此走向失敗。
因此,如何劃分服務,這是 DDD 非常有價值的一個地方,在分散式系統中,DDD 起到的作用實際上就是指導垂直拓展。值得慶幸的是,應用系統分散式級別增加帶來很多技術挑戰,但是邏輯上的架構變化卻不大。
在每一個不同的演化層次下,談 DDD 的程式碼架構才有意義。例如單體系統沒有必要過多分層,避免樣板程式碼大量出現;微服務系統則需要小心分層,並嚴格執行,否則修改成本非常高。另外也需要解決該層次下的技術問題,微服務需要解決分散式事務問題、分散式授權問題、分散式快取問題、效能問題等。
DDD 分層和職責在 DDD 指導程式碼設計部分,我們提到了三層架構和 DDD 的四層架構的區別,DDD 的四層架構被越來越多的認可,但是每層具體的職責很少有文章談到。根據實踐經驗,我把四層模型中具體的職責整理出來,用於團隊在做架構設計中能有共同的認識。
前面的 DDD 四層模型的圖為了表達每層中的元素,丟失了一個重要的角度,每一層的元件可能有多個。還是以收銀機系統為例,架構會是像下面這樣,業界大多數網際網路架構圖也是這樣畫的,只是使用術語略有不同。
實踐中我們發現,接入層是由應用場景解決的,因此接入層需要在特定應用場景下使用。收銀機應用下,接入層是 Restful API 以及 socket 連線實現的實時通訊,商戶管理和平臺管理無需使用這些接入方法,在不前後端分離的情況下,模板引擎也足夠使用。
同樣的,基礎設施層是和領域層繫結到一起用於實現業務邏輯和規則,底層基礎設施的選擇由領域層決定。商品服務主要是和資料庫打交道,需要使用 Mybatis,但是使用者認證服務(圖上未體現)可能只需要 Redis 做分散式會話即可。
接入層和技術設施層,更應該看做兩個亞層。結合 DDD 術語將示例圖調整如下:
應用層餐飲系統是一個非常複雜,具有多端、多租戶的系統,往往有收銀機應用、手機點餐應用、商戶管理、平臺管理等應用,從而組合成一個系統。在有些公司的語境裡,應用層往往是根據使用者角色劃分的,被稱為”業務面“。
應用層的特點:
關心處理完一個完整的業務該層只負責業務編排,物件轉換,實際業務邏輯由領域層完成不關心請求從何處來,但是關心誰來、做什麼、有沒有許可權做整合不同的領域服務解決問題最終一致性(最終一致性對業務有侵入)事務放到這層對應到分散式系統中的中臺等概念方法級別的功能許可權控制放到這層只產應用異常,對應 HTTP 狀態碼 403、401準單體系統下,按照應用劃分模組接入層對接入層來說,我們可以看到,實際上接入層是依附於應用層存在的,隨著前後端分離,Restful API 成了主流,對簡單的系統來說這一層越來越弱化。對於有終端接入的系統來說,接入層並不簡單,需要處理各種協議適配:XMPP、websocket、MQTT 等。在複雜度不高的情況下,我們往往把接入層和應用層合併部署,這裡往往憑經驗來決定。如果對分散式級別有了認識,可以更為科學的選擇是否要將接入層和應用部署到一起。
接入層的特點:
關心檢視和對外的服務,Restful、頁面渲染、websocket、XMPP 連線等如果沒有多種接入方式,可以和應用層合併對應到分散式系統中的閘道器、BFF、前臺等概念只產生接入異常,例如資料校驗,對應 HTTP 狀態碼 400、415 等一個應用可以有多個接入層接入層做和業務規則無關的 bean validation 驗證準單體系統下,按照連線方式分包領域層對於領域層來說,很多網際網路公司沒有這個概念,將這些實現混合在應用層隱藏實現了,造成業務規則不一致。隨著前後端分離的發展,2013 年左右我也開始前後端分離實踐,接入層剝離出去後,後端開發者開始審視是否需要抽象出一層來複用業務邏輯。當時大部分網際網路公司稱為服務,也就是 SOA 架構,大量使用 XML 和 SOAP 技術。
領域層的特點:
不關心場景,關心模型完整性和業務規則不關心誰來,不關心場景完整的業務,關心當前上下文的業務完整強一致性事務放到這層,聚合的事務是 "理所當然的"對應到分散式系統中的 domain service、後臺等概念領域層做業務規則驗證產生業務規則異常,例如使用者退款條件不滿足,對應狀態碼 412、419 等資料許可權放到這層(比如只允許刪除自己建立的商品),因為資料許可權涉及業務規則準單體系統下按照上下文分包,上下文之間呼叫必須走領域 domain service,目的就是解耦上下文中分聚合,聚合根要足夠小,只允許聚合根擁有對應的 domain service根據業務情況,參考反正規化理論,跨上下文使用值物件做必要的資料冗餘基礎設施層對於基礎設施層來說,技術設施層並不是指 MySQL、Redis 等外部元件,而是外部元件的介面卡,Hibernate、Mybatis、Redis Template 等,因此在 DDD 中介面卡模式被多次提到,基礎設施層往往不能單獨存在,還是要依附於領域層。技術設施層的介面卡還包括了外部系統的適配,網際網路產品系統的外部系統非常多,常見的有活體監測、風控系統、稅務發票等。
技術設施層的特點:
關心儲存、通知、第三方系統等外部設施(防腐層隔離)如果使用自動化的 ORM,這層可以在一定程度上省略基礎設施異常,應丟出內部異常,對應狀態碼 500準單體系統下按照 adapter 分包基礎設施的許可權由配置到應用的憑證控制,例如資料庫、物件儲存的憑證,技術設施層不涉及使用者的許可權DDD 分層的注意事項DDD 分層架構需要認識到一點是,有時候我們在專案中找不到每層之間的明顯的界限,那是因為我們使用的框架幫我們完成某一層。MVC 框架,Spring MVC、Jersey 幫我們搞定了接入層的事情,Hibernate、Redis Template 讓我們感覺不到基礎設施層。四層模型並不是一個刻板的教條,應該和你選用的框架做出調整,DDD 的作者也多次強調這一點。
另外,基礎設施層和接入層需要注意兩點:
接入層指的是服務端用於適配端側的部分,而非端側本身。因為接入層本來就依賴應用層,沒有人使用介面在這裡做依賴倒置,所有又被稱作主動適配。基礎設施層指的是適配基礎設施的部分,而非基礎設施本身。開發者往往希望資料訪問的介面有應用來定義,避免和基礎設施繫結,提供替換的可能,因此這裡往往大量使用介面,會有一些依賴倒置的實現,所以又被稱作為被動適配。關於依賴倒置的知識,可以瞭解面向物件的一些基礎概念。DDD 分層到四種架構的對映我們把這四層合到一起部署就是準單體系統,分開部署就是微服務、SOA。
更加有意思的是,在準單體系統中,如果我們嚴格限定領域層中模組之間的耦合關係,應用層訪問領域層是透過本地方法呼叫的。當我們想改造成微服務實現時,只需要簡單的抽象一個介面,然後透過遠端呼叫實現它,無論是 RPC、還是 Restful 訪問都不是大問題。
當然我們得解決遠端呼叫後的一系列問題,以及領域層是解耦良好的。
準單體系統準單體系統架構下,所有的程式碼在一個程式碼倉庫,四層架構依然,往往透過多模組組織程式碼。應用層透過不同的模組實現,然後將領域服務抽出來一個公用模組。很多小型專案依然保持這種形態,每層能保持良好的依賴關係非常重要。 每層之間最好依次向下呼叫,DDD 的書中有一個不好的示例,上層可以跳過中間層直接呼叫下層。
很多內網部署的傳統專案單機就能滿足,小型公司的 OA 軟體、餐飲軟體、會員管理系統的單機版就是透過這種方式部署。
低級別分散式系統將應用水平拓展,資料庫進行主從拆分,Redis 使用主從或哨兵模式,本質上和準單體系統沒有區別,應用沒有垂直拓展複雜性不會有特別大的提升。
還有一種折中的方式,應用層各個模組單獨部署,領域層的業務邏輯單獨部署或者透過 Jar 包的方式載入應用中,實現應用層的解耦,並且不會帶來分散式的問題。
基於上面這種模式的變體,下面這種部署方式也有很多,透過這種部署方式,領域服務使用嚴謹的 Java 實現,接入層和應用層使用 PHP、Nodejs 等動態語言實現。
高級別分散式系統如果我們把應用和領域層都獨立部署,就得到了現在主流的微服務架構。只不過在微服務的語境下,應用層 + 接入層被稱為 BFF (Backend for Frontend),領域層負責實現業務邏輯,應用層用於各種業務場景下的適配。
然而這種設計會受到一些批評,他們認為這不是正宗的微服務,而像現在所說的中臺。部分微服務的工程師倡導使用 API Gateway 的方式將領域服務的 API 直接暴露給端側。
實際上這種做法應用層並沒有消失,編排領域服務 API 的職責被下放到端側,在一些特殊的業務場景下沒有問題,但是大多數場景下並不合適。業務邏輯容易造成碎片化,存在呼叫次數多,服務間最終一致性事務難以實現等問題。下面這張圖表達了這種設計方式,但大多數情況下並不推薦。
到此,領域層被垂直拆分,隨之而來的就是我們熟知的各種分散式問題了,熔斷、負載均衡等問題屬於技術複雜度可以在業務無感知的情況下被解決,但下面幾個問題需要侵入業務才能被良好的解決,因此還需要 DDD 的幫助。
領域層模組之間的事務怎麼處理?領域層模組之間需要表關聯怎麼辦?領域層是無狀態的,怎麼做許可權控制?領域層模組之間的依賴關係怎麼處理?我們在後面的 《DDD 指導應用垂直拆分後的問題》部分回答。
複雜分散式系統高級別的分散式系統已經是業界大的網際網路公司的主流做法,不過在一些極端複雜的系統中,依然不能滿足業務需要。倒不是技術上一定要拆的非常細,主要是參與開發的人數多、程式碼量大,團隊協作、版本構建有很多問題。
一個最佳的敏捷團隊為 10 到 15人,除去測試、業務分析師,開發者一般在 10 人左右。因此在非常複雜的系統中儘可能把能拆分的都拆出去。繼續拆分往往有兩個方向:
變得複雜的接入層,在應用層裡面兜不住了。例如 socket 連線相當費資源,可以剝離出去單獨建立連線,然後和收銀機應用通訊。一些外部系統的適配層,例如簡訊閘道器、稅務系統適配服務。某大型 lot 平臺將對接端側的服務根據接入協議拆分,HTTP、MQTT、XMPP 然後轉換資料格式後統一送入。不過,這種場景已經比較少見。