領域驅動模型DDD(一)——服務拆分策略

阿波羅的手發表於2022-03-23

前言

領域驅動模型設計在業界也喊了幾年口號了,但是對於很多“務實”的程式設計師來說,紙上談“術”遠比敲程式碼難得太多太多。本人能力有限,在拜讀相關作品時既要隱忍書中晦澀難懂的專業名詞,又要去迎合西方大牛在撰寫的過程中融入的西式故事。我想總會有一部分水平和我類似的碼農們,需要一份對系統闡述DDD小白文化的文字。因此,本人便自不量力地結合一些簡單的專案經驗,將領域驅動模型設計思想從理解到落地的實施和總結分享給諸位。當然,如果是某些行業先鋒不幸看到本人稚嫩的文字時,就當作是馬戲團中的小丑,一笑了之翻頁便可。

思維的入門

在學習架構思想的初期,特別時面對架構模型時(例如六邊形架構、領域服務拆分),我總會不自然地在對號入座,思考在模型中的這一塊放到程式碼實現上是Controller層還是Dao層,是採用訊息中介軟體還是NoSQL快取。這種自動聯想的“被動技能”在學習微服務設計架構思想的過程中是致命的,過於專注業務的技術實現而脫離架構思想本身的大局觀,就容易陷入用“具體”無法概括“抽象”的處境。

對此,讓我們先忘了平日在開發中使用的各種被分類到細緻入微的技術,帶著一種閱讀“無用”文學而非可以模仿實操的工具書的心態一起來對領域驅動設計做一個基礎性的理解。

1 常用的服務拆分方法

1.1 根據業務能力進行服務拆分

建立微服務架構的策略之一就是採用業務能力進行服務拆分,這也是目前市面上大部分產品設計初期採用的方式。主要原因是這種方法對於架構師來說是比較容易實施的,就好像在做一個分類遊戲,關於使用者的註冊登入以及資訊管理可以劃分為一個業務,關於文章的釋出下架以及內容更迭可以劃分為一個業務。

這樣的分類手段核心是以業務活動進行劃分,也保證了大部分物件導向的程式設計師在程式碼實現時可以更快地、更明確地建立實體類,從而開闢出一個個針對不同業務功能的微服務。

倘若我們以程式設計師盤踞的技術論壇/部落格為例,那麼它們必須要實現最基礎的三大業務:1、使用者的註冊登入以及使用者對個人資訊的操作;2、使用者可以釋出/修改/刪除自己的文章;3、網站的後臺管理員可以對前面兩者進行更高許可權的操作。至於更多的使用者評論、使用者私聊、vip充值等一些功能則是擴充,畢竟沒有它們也不會影響到整個網站的正常運作,因此暫不加入討論。

所以根據上述最基礎的三大業務,如果要進行微服務架構設計(當然現實中不會有為了如此簡陋的網站採用微服務,不然會被從業的程式設計師背地挖苦痛罵),我們可以畫出相應的對映圖【圖1-1】:

採用業務能力進行拆分固然方便了架構師和程式設計師,在應對一般的企業專案時,這種方式是非常穩妥的方案,架構中每個劃分出的服務內部後期可以根隨功能的需求增加而逐漸迭代(例如,我現在需要文章釋出時可以附帶圖片,那麼可以在實現文章業務的服務中新增相關的介面與具體實現程式碼),但是整體的專案架構是保持不變的。

話雖如此,但讀者們要知道上面僅僅是為了實現技術部落格網站最基本業務而定義的“初代”架構。一種情況是隨著整個技術部落格網站的日益龐大,為了迎合使用者的需要,我們不得不擴充套件網站的功能性,例如增添使用者對文章點贊/收藏/留言,後臺對文章內容是否健康的稽核等等,這時候初代被劃分好的服務內部就會日益“龐大”,需要我們重新操刀對其進行分解切割。

另外一種情況是由於使用者量的提升,致使服務之間的遠端呼叫、程式間的通訊次數驟增而導致請求響應效率逐步低下,例如擁有龐大數量的讀者使用者在訪問文章時不僅要通過文章服務獲取文章內容,還要通過文章的作者每次呼叫使用者服務來獲取作者個人資訊簡介(在沒有快取機制,每個服務根據業務主體分割明確的假設情況),我們又得考慮把一些服務組合在一起:

例如當我們現在常用的註冊/登入手段——利用第三方服務進行簡訊驗證,如果只是為了實現使用者註冊/登入功能,完全可以併入到使用者的服務中【圖1-2】:

一旦不單使用者登入/註冊情況用到簡訊驗證,例如要充值時候也要用到簡訊驗證支付,那麼就得把它獨立出來【圖1-3】:

上面的問題正是以業務進行服務拆分時難以避免的,因為業務必然是不斷髮展的(參考市面上眾多臃腫的軟體),被繁雜的業務牽著鼻子走的架構模式必然會陷入到不停拆分或重組的境地,那麼服務內部以及服務之間的關係也將逐步模糊紊亂。

1.2 根據子域進行服務拆分

即使這是一段很枯燥的歷史,但為了感謝曾在這個領域躬耕的技術先驅們我還是要帶上這段文字:Eric Evans在他的經典著作中(Addison-Wesley Professional)提出的領域驅動設計是構建複雜軟體的方法論,這些軟體通常都是以物件導向和領域模型為核心。

既然是方法論,它提供的是“怎麼辦”的理論體系。類比蛋炒韭菜,我所能提供的是做這道菜需要什麼工具和佐料,放油放蛋放鹽放韭菜的先後順序和比例,至於真正做起來火候的大小,炒菜的姿勢,蛋菜的克量等都是視情況而定,並沒有固定的要求。這又回到我在“思維入門”中提到的,不要用機械的“具體”去反推“抽象”理論,因為炒菜的姿勢最多能影響的是菜的口味,並非是完成這道菜的必要條件。

下面讓我們來理解DDD中重要的兩個概念:子域和界限上下文。我先以好理解的方式在各位腦海中勾勒出這兩者的基本認識,然後各位再去看專業解釋會好接受的多。

如果把一個專案業務比作一個國家整體,而子域相當於國家內部的各個省份,這些省份細緻地劃分了國家每個地域面積大小和居住人群,而所有省份的聚合又構成了整個國家。作為限界上下文,其英文為bounded context,可以直譯成“有界的環境”,那麼套入前文各位可能舉一反三理解成“省界”。但事實並非如此,以“省份”類比子域的話,省界並非是限制人口流動的主要原因,限制人流的本質在於一個省份有一個省份的風土人情和文化語言,例如廣東省用粵語交流,福建省用閩南語交流,這些語言在省份內部是大家達成共識都能理解的,但跑到廣東說福建話只會讓本地人摸不著頭腦。

因此,每個省份(子域)之間產生的交流障礙歸根於它們各自內部通用語言環境不同(限界上下文)。這個舉例或許不符合社會學對於人口流動的分析,但拿來解釋子域和限界上下文還是很貼切的。

那麼讓我們引入教科書對二者的解釋來鞏固諸位對它們的記憶:子域是領域的一部分,領域是DDD中用來描述應用程式問題域的一個術語。識別子域的方法和識別業務能力一樣:分析業務並識別業務的不同專業領域,分析產生的子域定義結果也會和識別業務能力得到的結果非常相近。DDD把領域設計模型的邊界成為界限上下文,當使用微服務架構時,每一個界限上下文對應一個或則一組服務,我們可以通過DDD方式定義子域,並把子域對應為每一個服務【圖1-4】。

1.3 拆分單體應用的難點

1.網路延時

網路延時是分散式系統中一直存在的問題。不論是以業務進行拆分還是子域進行拆分,對服務不斷細化分解會導致各個服務之間的大量往返呼叫。即使可以通過批量處理API在一次往返中獲取多個物件,從而減少延時。但是在其他情況下,解決方案是把多個服務整合到一起,用變成語言中的函式呼叫替換昂貴的程式通訊。

2.同步程式間通訊導致可用性降低

舉一個最簡單的例子,當我們在某東下單一件商品時建立了訂單,建立訂單的過程中需要獲取商品的詳細資訊和購買者的詳細資訊,而這其中有一個服務出現了不可用的狀態就會導致整個業務建立失敗,這種同步程式通訊帶來的可用性降低讓我們不得不折中採用非同步訊息進行處理。

3.服務之間維持資料的一致性

當我們對服務進行拆分後,服務之間如何保持資料一致性成為重點和難點。還是以某東為例,在活動期間大量使用者可能在同一刻參與了秒殺活動,而對於倉存服務與商品服務之間如何保證它們在數量的一致性(比如前臺有100個使用者下單,那麼對於商品來說只需要在原有的數量上減去100然後返回給前端頁面作為商品資訊的一部分及時展示給使用者還有多少存貨便可,但是實際上真正需要扣減的應該發生在倉庫服務中,因為倉庫服務記憶體儲的才是實際庫存),怎麼讓一波狂歡後實際庫存與前端保持一致是業務中必須攻克的難題。

4.上帝類阻礙了服務的拆分

分解的一部分障礙就是所謂的上帝類,即全域性類或則是“公用”類。上帝類通常為應用程式不同方面實現業務邏輯。

以美團外賣業務舉例,一個不經思考設計的訂單類中會將以下所有資訊屬性直接構建成一個類:商家資訊屬性(商家名稱、商家地址等)、使用者資訊屬性(賬戶、暱稱、地址等)、外賣商品屬性(外賣商品名稱、價格、配料等)、配送方屬性(配送員身份、配送員電話號碼、配送起始時間、配送截止時間等)。不過這些屬性涉及了不同服務中的應用程式,導致了商品系統中必然存在的訂單所包含的資訊變得特別龐大,並且與其他服務之間因為共同的屬性保持著“曖昧”的聯絡。

一種解決方法是在資料庫中創立一個公用的訂單資料庫,處理訂單的所有服務使用此資料庫,但這就出現了“緊耦合”的情況。

對此應用DDD將每個服務視為“孤島”般的子域(儘量不與其他服務發生在屬性的上糾葛,形成一座具有特色便於識別的“孤島”)。所以讓我們看看自己手機上美團APP中客戶訂單,然後再看看拿到外賣時釘在外賣上的商家收到的訂單,幸運的話再看看外賣小哥送外賣時接收的訂單,它們所包含的資訊內容和資訊數量絕對是不同。

這代表著在商家子域、使用者子域、配送子域中我們根據子域的不同定義了不同側重點的“訂單”,而不是一股腦地都塞進全域性訂單類裡讓不同服務進行共享。也正是不同側重點的“訂單”只有在自己子域中才是“有效的”,“能被讀懂的”(總不可能讓商家去看外賣小哥的訂單資訊,使用者去看商家的訂單資訊),子域便有了與之對應的界限上下文。

這一章的基礎內容便到此結束,下一章可能會講一下服務的程式通訊或則是Saga管理事務,盡情期待。

相關文章